Repository: motis-project/motis Branch: master Commit: 2b88cb38965c Files: 443 Total size: 18.0 MB Directory structure: gitextract_8b0p7zjl/ ├── .clang-format ├── .clang-tidy.in ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .pkg ├── CMakeLists.txt ├── CMakePresets.json ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── cmake/ │ ├── buildcache.cmake │ ├── clang-tidy.cmake │ └── pkg.cmake ├── docs/ │ ├── STYLE.md │ ├── dev-setup-server.md │ ├── elevation-setup.md │ ├── linux-dev-setup.md │ ├── macos-dev-setup.md │ ├── python-client.md │ ├── scripting.md │ ├── setup.md │ └── windows-dev-setup.md ├── exe/ │ ├── batch.cc │ ├── compare.cc │ ├── extract.cc │ ├── flags.h │ ├── generate.cc │ ├── main.cc │ └── params.cc ├── include/ │ └── motis/ │ ├── adr_extend_tt.h │ ├── analyze_shapes.h │ ├── box_rtree.h │ ├── clog_redirect.h │ ├── compute_footpaths.h │ ├── config.h │ ├── constants.h │ ├── ctx_data.h │ ├── ctx_exec.h │ ├── data.h │ ├── direct_filter.h │ ├── elevators/ │ │ ├── elevators.h │ │ ├── get_state_changes.h │ │ ├── match_elevator.h │ │ ├── parse_elevator_id_osm_mapping.h │ │ ├── parse_fasta.h │ │ ├── parse_siri_fm.h │ │ └── update_elevators.h │ ├── endpoints/ │ │ ├── adr/ │ │ │ ├── filter_conv.h │ │ │ ├── geocode.h │ │ │ ├── reverse_geocode.h │ │ │ └── suggestions_to_response.h │ │ ├── elevators.h │ │ ├── graph.h │ │ ├── gtfsrt.h │ │ ├── initial.h │ │ ├── levels.h │ │ ├── map/ │ │ │ ├── flex_locations.h │ │ │ ├── rental.h │ │ │ ├── route_details.h │ │ │ ├── routes.h │ │ │ ├── shapes_debug.h │ │ │ ├── stops.h │ │ │ └── trips.h │ │ ├── matches.h │ │ ├── metrics.h │ │ ├── ojp.h │ │ ├── one_to_all.h │ │ ├── one_to_many.h │ │ ├── one_to_many_post.h │ │ ├── osr_routing.h │ │ ├── platforms.h │ │ ├── routing.h │ │ ├── stop_times.h │ │ ├── tiles.h │ │ ├── transfers.h │ │ ├── trip.h │ │ └── update_elevator.h │ ├── flex/ │ │ ├── flex.h │ │ ├── flex_areas.h │ │ ├── flex_output.h │ │ ├── flex_routing_data.h │ │ └── mode_id.h │ ├── fwd.h │ ├── gbfs/ │ │ ├── compression.h │ │ ├── data.h │ │ ├── gbfs_output.h │ │ ├── geofencing.h │ │ ├── lru_cache.h │ │ ├── mode.h │ │ ├── osr_mapping.h │ │ ├── osr_profile.h │ │ ├── parser.h │ │ ├── partition.h │ │ ├── routing_data.h │ │ └── update.h │ ├── get_loc.h │ ├── get_stops_with_traffic.h │ ├── hashes.h │ ├── http_req.h │ ├── import.h │ ├── journey_to_response.h │ ├── location_routes.h │ ├── logging.h │ ├── match_platforms.h │ ├── metrics_registry.h │ ├── motis_instance.h │ ├── odm/ │ │ ├── bounds.h │ │ ├── journeys.h │ │ ├── meta_router.h │ │ ├── odm.h │ │ ├── prima.h │ │ ├── query_factory.h │ │ ├── shorten.h │ │ └── td_offsets.h │ ├── osr/ │ │ ├── max_distance.h │ │ ├── mode_to_profile.h │ │ ├── parameters.h │ │ └── street_routing.h │ ├── parse_location.h │ ├── place.h │ ├── point_rtree.h │ ├── polyline.h │ ├── railviz.h │ ├── route_shapes.h │ ├── rt/ │ │ ├── auser.h │ │ └── rt_metrics.h │ ├── rt_update.h │ ├── server.h │ ├── tag_lookup.h │ ├── tiles_data.h │ ├── timetable/ │ │ ├── clasz_to_mode.h │ │ ├── modes_to_clasz_mask.h │ │ └── time_conv.h │ ├── transport_mode_ids.h │ ├── tt_location_rtree.h │ ├── types.h │ └── update_rtt_td_footpaths.h ├── openapi.yaml ├── src/ │ ├── adr_extend_tt.cc │ ├── analyze_shapes.cc │ ├── clog_redirect.cc │ ├── compute_footpaths.cc │ ├── config.cc │ ├── data.cc │ ├── direct_filter.cc │ ├── elevators/ │ │ ├── elevators.cc │ │ ├── match_elevators.cc │ │ ├── parse_elevator_id_osm_mapping.cc │ │ ├── parse_fasta.cc │ │ ├── parse_siri_fm.cc │ │ └── update_elevators.cc │ ├── endpoints/ │ │ ├── adr/ │ │ │ ├── filter_conv.cc │ │ │ ├── geocode.cc │ │ │ ├── reverse_geocode.cc │ │ │ └── suggestions_to_response.cc │ │ ├── elevators.cc │ │ ├── graph.cc │ │ ├── gtfsrt.cc │ │ ├── initial.cc │ │ ├── levels.cc │ │ ├── map/ │ │ │ ├── flex.cc │ │ │ ├── rental.cc │ │ │ ├── route_details.cc │ │ │ ├── routes.cc │ │ │ ├── shapes_debug.cc │ │ │ ├── stops.cc │ │ │ └── trips.cc │ │ ├── matches.cc │ │ ├── metrics.cc │ │ ├── ojp.cc │ │ ├── one_to_all.cc │ │ ├── one_to_many.cc │ │ ├── one_to_many_post.cc │ │ ├── osr_routing.cc │ │ ├── platforms.cc │ │ ├── routing.cc │ │ ├── stop_times.cc │ │ ├── tiles.cc │ │ ├── transfers.cc │ │ ├── trip.cc │ │ └── update_elevator.cc │ ├── flex/ │ │ ├── flex.cc │ │ ├── flex_areas.cc │ │ └── flex_output.cc │ ├── gbfs/ │ │ ├── data.cc │ │ ├── gbfs_output.cc │ │ ├── geofencing.cc │ │ ├── mode.cc │ │ ├── osr_mapping.cc │ │ ├── osr_profile.cc │ │ ├── parser.cc │ │ ├── routing_data.cc │ │ └── update.cc │ ├── get_stops_with_traffic.cc │ ├── hashes.cc │ ├── http_req.cc │ ├── import.cc │ ├── journey_to_response.cc │ ├── logging.cc │ ├── match_platforms.cc │ ├── metrics_registry.cc │ ├── odm/ │ │ ├── blacklist_taxi.cc │ │ ├── bounds.cc │ │ ├── journeys.cc │ │ ├── meta_router.cc │ │ ├── odm.cc │ │ ├── prima.cc │ │ ├── query_factory.cc │ │ ├── shorten.cc │ │ ├── td_offsets.cc │ │ ├── whitelist_ridesharing.cc │ │ └── whitelist_taxi.cc │ ├── osr/ │ │ ├── max_distance.cc │ │ ├── mode_to_profile.cc │ │ ├── parameters.cc │ │ └── street_routing.cc │ ├── parse_location.cc │ ├── place.cc │ ├── polyline.cc │ ├── railviz.cc │ ├── route_shapes.cc │ ├── rt/ │ │ └── auser.cc │ ├── rt_update.cc │ ├── server.cc │ ├── tag_lookup.cc │ ├── timetable/ │ │ ├── clasz_to_mode.cc │ │ └── modes_to_clasz_mask.cc │ └── update_rtt_td_footpaths.cc ├── test/ │ ├── combinations_test.cc │ ├── config_test.cc │ ├── elevators/ │ │ ├── parse_elevator_id_osm_mapping_test.cc │ │ ├── parse_fasta_test.cc │ │ ├── parse_siri_fm_test.cc │ │ └── siri_fm_routing_test.cc │ ├── endpoints/ │ │ ├── map_routes_test.cc │ │ ├── ojp_test.cc │ │ ├── one_to_many_test.cc │ │ ├── siri_sx_test.cc │ │ ├── stop_group_geocoding_test.cc │ │ ├── stop_times_test.cc │ │ └── trip_test.cc │ ├── ffm_hbf.osm │ ├── flex_mode_id_test.cc │ ├── gbfs_partition_test.cc │ ├── main.cc │ ├── matching_test.cc │ ├── odm/ │ │ ├── csv_journey_test.cc │ │ ├── prima_test.cc │ │ └── td_offsets_test.cc │ ├── read_test.cc │ ├── resources/ │ │ ├── gbfs/ │ │ │ ├── free_bike_status.json │ │ │ ├── gbfs.json │ │ │ ├── geofencing_zones.json │ │ │ ├── station_information.json │ │ │ ├── station_status.json │ │ │ ├── system_information.json │ │ │ └── vehicle_types.json │ │ ├── ojp/ │ │ │ ├── geocoding_request.xml │ │ │ ├── geocoding_response.xml │ │ │ ├── intermodal_routing_request.xml │ │ │ ├── intermodal_routing_response.xml │ │ │ ├── map_stops_request.xml │ │ │ ├── map_stops_response.xml │ │ │ ├── routing_request.xml │ │ │ ├── routing_response.xml │ │ │ ├── stop_event_request.xml │ │ │ ├── stop_event_response.xml │ │ │ ├── trip_info_request.xml │ │ │ └── trip_info_response.xml │ │ └── test_case.geojson │ ├── routing_shrink_results_test.cc │ ├── routing_slow_direct_test.cc │ ├── routing_test.cc │ ├── tag_lookup_test.cc │ ├── test_case/ │ │ └── .gitignore │ ├── test_case.cc │ ├── test_case.h │ ├── test_dir.h.in │ ├── util.cc │ └── util.h ├── tools/ │ ├── buildcache-clang-tidy.lua │ ├── suppress.txt │ ├── try-reproduce.py │ └── ubsan-suppress.txt └── ui/ ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── api/ │ ├── LICENSE │ ├── README.md │ ├── openapi/ │ │ ├── index.ts │ │ ├── schemas.gen.ts │ │ ├── services.gen.ts │ │ └── types.gen.ts │ ├── package.json │ └── tsconfig.json ├── components.json ├── eslint.config.js ├── package.json ├── playwright.config.ts ├── pnpm-workspace.yaml ├── postcss.config.js ├── src/ │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── index.test.ts │ ├── lib/ │ │ ├── AddressTypeahead.svelte │ │ ├── AdvancedOptions.svelte │ │ ├── Alerts.svelte │ │ ├── Color.ts │ │ ├── ConnectionDetail.svelte │ │ ├── DateInput.svelte │ │ ├── Debug.svelte │ │ ├── DeparturesMask.svelte │ │ ├── DirectConnection.svelte │ │ ├── ErrorMessage.svelte │ │ ├── IsochronesInfo.svelte │ │ ├── IsochronesMask.svelte │ │ ├── ItineraryList.svelte │ │ ├── LevelSelect.svelte │ │ ├── Location.ts │ │ ├── Modes.ts │ │ ├── NumberSelect.svelte │ │ ├── Precision.ts │ │ ├── RailViz.svelte │ │ ├── Route.svelte │ │ ├── SearchMask.svelte │ │ ├── StopTimes.svelte │ │ ├── StreetModes.svelte │ │ ├── Time.svelte │ │ ├── TransitModeSelect.svelte │ │ ├── ViaStopOptions.svelte │ │ ├── components/ │ │ │ └── ui/ │ │ │ ├── button/ │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── card/ │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-header.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ ├── card.svelte │ │ │ │ └── index.ts │ │ │ ├── dialog/ │ │ │ │ ├── dialog-content.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ └── index.ts │ │ │ ├── label/ │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ ├── radio-group/ │ │ │ │ ├── index.ts │ │ │ │ ├── radio-group-item.svelte │ │ │ │ └── radio-group.svelte │ │ │ ├── select/ │ │ │ │ ├── index.ts │ │ │ │ ├── select-content.svelte │ │ │ │ ├── select-group-heading.svelte │ │ │ │ ├── select-item.svelte │ │ │ │ ├── select-scroll-down-button.svelte │ │ │ │ ├── select-scroll-up-button.svelte │ │ │ │ ├── select-separator.svelte │ │ │ │ └── select-trigger.svelte │ │ │ ├── separator/ │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ ├── switch/ │ │ │ │ ├── index.ts │ │ │ │ └── switch.svelte │ │ │ ├── table/ │ │ │ │ ├── index.ts │ │ │ │ ├── table-body.svelte │ │ │ │ ├── table-caption.svelte │ │ │ │ ├── table-cell.svelte │ │ │ │ ├── table-footer.svelte │ │ │ │ ├── table-head.svelte │ │ │ │ ├── table-header.svelte │ │ │ │ ├── table-row.svelte │ │ │ │ └── table.svelte │ │ │ ├── tabs/ │ │ │ │ ├── index.ts │ │ │ │ ├── tabs-content.svelte │ │ │ │ ├── tabs-list.svelte │ │ │ │ ├── tabs-trigger.svelte │ │ │ │ └── tabs.svelte │ │ │ └── toggle/ │ │ │ ├── index.ts │ │ │ └── toggle.svelte │ │ ├── constants.ts │ │ ├── defaults.ts │ │ ├── formatDuration.ts │ │ ├── generateTimes.ts │ │ ├── getModeName.ts │ │ ├── i18n/ │ │ │ ├── bg.ts │ │ │ ├── cs.ts │ │ │ ├── de.ts │ │ │ ├── en.ts │ │ │ ├── fr.ts │ │ │ ├── pl.ts │ │ │ └── translation.ts │ │ ├── lngLatToStr.ts │ │ ├── map/ │ │ │ ├── Control.svelte │ │ │ ├── Drawer.svelte │ │ │ ├── GeoJSON.svelte │ │ │ ├── Isochrones.svelte │ │ │ ├── IsochronesShapeWorker.ts │ │ │ ├── IsochronesShared.ts │ │ │ ├── IsochronesWorker.ts │ │ │ ├── Layer.svelte │ │ │ ├── Map.svelte │ │ │ ├── Marker.svelte │ │ │ ├── Popup.svelte │ │ │ ├── colors.ts │ │ │ ├── createTripIcon.ts │ │ │ ├── getModeLabel.ts │ │ │ ├── handleScroll.ts │ │ │ ├── itineraries/ │ │ │ │ ├── ItineraryGeoJSON.svelte │ │ │ │ ├── itineraryLayers.ts │ │ │ │ └── layerFilters.ts │ │ │ ├── rentals/ │ │ │ │ ├── Rentals.svelte │ │ │ │ ├── StationPopup.svelte │ │ │ │ ├── VehiclePopup.svelte │ │ │ │ ├── ZoneLayer.svelte │ │ │ │ ├── ZonePopup.svelte │ │ │ │ ├── assets.ts │ │ │ │ ├── style.ts │ │ │ │ ├── zone-fill-layer.ts │ │ │ │ └── zone-types.ts │ │ │ ├── routes/ │ │ │ │ └── Routes.svelte │ │ │ ├── shield.ts │ │ │ ├── stops/ │ │ │ │ └── StopsGeoJSON.svelte │ │ │ └── style.ts │ │ ├── modeStyle.ts │ │ ├── preprocessItinerary.ts │ │ ├── toDateTime.ts │ │ ├── tripsWorker.ts │ │ ├── types.ts │ │ └── utils.ts │ └── routes/ │ ├── +layout.svelte │ ├── +layout.ts │ └── +page.svelte ├── static/ │ ├── sprite_sdf.json │ └── sprite_sdf@2x.json ├── svelte.config.js ├── tailwind.config.js ├── tests/ │ └── test.ts ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ BasedOnStyle: Google IndentWidth: 2 Language: Cpp DerivePointerAlignment: false PointerAlignment: Left AccessModifierOffset: -2 ConstructorInitializerAllOnOneLineOrOnePerLine: true AlignTrailingComments: false KeepEmptyLinesAtTheStartOfBlocks: true AllowShortCaseLabelsOnASingleLine: true AlwaysBreakTemplateDeclarations: true SpacesBeforeTrailingComments: 2 SortIncludes: true IncludeBlocks: Preserve BinPackParameters: false QualifierAlignment: Right IncludeCategories: - Regex: '^<.*>' Priority: 1 - Regex: '^"boost.*' Priority: 2 - Regex: '^"nigiri/core/common.*' Priority: 3 - Regex: '^"nigiri/core/schedule.*' Priority: 4 - Regex: '^"nigiri/core/.*' Priority: 5 - Regex: '^"nigiri/module/.*' Priority: 6 - Regex: '^"nigiri/bootstrap/.*' Priority: 7 - Regex: '^"nigiri/loader/.*' Priority: 8 - Regex: '^"nigiri/.*' Priority: 9 - Regex: '^"nigiri/protocol.*' Priority: 10 - Regex: '^"./.*' Priority: 11 - Regex: '.*' Priority: 12 --- BasedOnStyle: Google IndentWidth: 2 Language: ObjC DerivePointerAlignment: false PointerAlignment: Left AccessModifierOffset: -2 ConstructorInitializerAllOnOneLineOrOnePerLine: true AlignTrailingComments: false KeepEmptyLinesAtTheStartOfBlocks: true AllowShortCaseLabelsOnASingleLine: true AlwaysBreakTemplateDeclarations: true SpacesBeforeTrailingComments: 2 ColumnLimit: 80 QualifierAlignment: Right IncludeCategories: - Regex: '^<.*>' Priority: 1 - Regex: '^"boost.*' Priority: 2 - Regex: '^"nigiri/core/common.*' Priority: 3 - Regex: '^"nigiri/core/schedule.*' Priority: 4 - Regex: '^"nigiri/core/.*' Priority: 5 - Regex: '^"nigiri/module/.*' Priority: 6 - Regex: '^"nigiri/bootstrap/.*' Priority: 7 - Regex: '^"nigiri/loader/.*' Priority: 8 - Regex: '^"nigiri/.*' Priority: 9 - Regex: '^"nigiri/protocol.*' Priority: 10 - Regex: '^"./.*' Priority: 11 - Regex: '.*' Priority: 12 ================================================ FILE: .clang-tidy.in ================================================ Checks: "*,\ -llvmlibc-*,\ -abseil-*,\ -readability-identifier-length,\ -altera-unroll-loops,\ -altera-id-dependent-backward-branch,\ -bugprone-easily-swappable-parameters,\ -bugprone-implicit-widening-of-multiplication-result,\ -llvm-else-after-return,\ -hicpp-named-parameter,\ -cert-err60-cpp,\ -clang-analyzer-core.NonNullParamChecker,\ -misc-unused-parameters,\ -cppcoreguidelines-pro-bounds-array-to-pointer-decay,\ -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ -cppcoreguidelines-pro-type-union-access,\ -readability-simplify-boolean-expr,\ -clang-analyzer-alpha*,\ -google-build-using-namespace,\ -clang-analyzer-optin.osx*,\ -clang-analyzer-osx*,\ -readability-implicit-bool-cast,\ -readability-else-after-return,\ -llvm-include-order,\ -clang-analyzer-alpha.unix.PthreadLock,\ -llvm-header-guard,\ -readability-named-parameter,\ -clang-analyzer-alpha.deadcode.UnreachableCode,\ -cppcoreguidelines-pro-type-reinterpret-cast,\ -cppcoreguidelines-pro-type-vararg,\ -misc-move-const-arg,\ -google-runtime-references,\ -cert-err58-cpp,\ -modernize-use-default-member-init,\ -fuchsia-overloaded-operator,\ -fuchsia-default-arguments,\ -hicpp-vararg,\ -clang-analyzer-optin.cplusplus.VirtualCall,\ -cppcoreguidelines-owning-memory,\ -hicpp-no-array-decay,\ -*-magic-numbers,\ -*-non-private-member-variables-in-classes,\ -fuchsia-statically-constructed-objects,\ -readability-isolate-declaration,\ -fuchsia-multiple-inheritance,\ -fuchsia-trailing-return,\ -portability-simd-intrinsics,\ -modernize-use-nodiscard,\ -cppcoreguidelines-pro-bounds-constant-array-index,\ -*-avoid-c-arrays,\ -*-narrowing-conversions,\ -*-avoid-goto,\ -hicpp-multiway-paths-covered,\ -clang-analyzer-cplusplus.NewDeleteLeaks,\ -clang-analyzer-cplusplus.NewDelete,\ -hicpp-signed-bitwise,\ -cert-msc32-c,\ -cert-msc51-cpp,\ -bugprone-exception-escape,\ -cppcoreguidelines-macro-usage,\ -cert-dcl21-cpp,\ -modernize-use-trailing-return-type,\ -fuchsia-default-arguments-calls,\ -fuchsia-default-arguments-declarations,\ -misc-no-recursion,\ -llvmlibc-callee-namespace,\ -llvm-else-after-return,\ -llvm-qualified-auto,\ -readability-qualified-auto,\ -google-readability-avoid-underscore-in-googletest-name,\ -readability-function-cognitive-complexity,\ -readability-avoid-const-params-in-decls,\ -cppcoreguidelines-avoid-const-or-ref-data-members,\ -cppcoreguidelines-avoid-do-while,\ -altera-struct-pack-align,\ -bugprone-unchecked-optional-access,\ -readability-identifier-naming,\ -cert-dcl37-c,\ -bugprone-reserved-identifier,\ -cert-dcl51-cpp,\ -misc-confusable-identifiers,\ -clang-analyzer-optin.core.EnumCastOutOfRange,\ -clang-analyzer-core.CallAndMessage" WarningsAsErrors: '*' HeaderFilterRegex: '^${RELATIVE_SOURCE_DIR}(base|modules|test)/' AnalyzeTemporaryDtors: false UseColor: true User: root CheckOptions: - key: cert-err61-cpp.CheckThrowTemporaries value: '1' - key: cert-oop11-cpp.IncludeStyle value: llvm - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader value: '' - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle value: '0' - key: google-readability-braces-around-statements.ShortStatementLines value: '1' - key: google-readability-function-size.BranchThreshold value: '4294967295' - key: google-readability-function-size.LineThreshold value: '4294967295' - key: google-readability-function-size.StatementThreshold value: '800' - key: google-readability-namespace-comments.ShortNamespaceLines value: '10' - key: google-readability-namespace-comments.SpacesBeforeComments value: '2' - key: google-runtime-int.SignedTypePrefix value: int - key: google-runtime-int.TypeSuffix value: '' - key: google-runtime-int.UnsignedTypePrefix value: uint - key: llvm-namespace-comment.ShortNamespaceLines value: '1' - key: llvm-namespace-comment.SpacesBeforeComments value: '2' - key: misc-assert-side-effect.AssertMacros value: assert - key: misc-assert-side-effect.CheckFunctionCalls value: '0' - key: misc-definitions-in-headers.UseHeaderFileExtension value: '1' - key: misc-move-constructor-init.IncludeStyle value: llvm - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries value: '1' - key: modernize-loop-convert.MaxCopySize value: '16' - key: modernize-loop-convert.MinConfidence value: reasonable - key: modernize-loop-convert.NamingStyle value: lower_case - key: modernize-pass-by-value.IncludeStyle value: llvm - key: modernize-replace-auto-ptr.IncludeStyle value: llvm - key: modernize-use-nullptr.NullMacros value: 'NULL' - key: readability-braces-around-statements.ShortStatementLines value: '0' - key: readability-function-size.BranchThreshold value: '4294967295' - key: readability-function-size.LineThreshold value: '4294967295' - key: readability-function-size.StatementThreshold value: '800' - key: readability-identifier-naming.AbstractClassCase value: lower_case - key: readability-identifier-naming.AbstractClassPrefix value: '' - key: readability-identifier-naming.AbstractClassSuffix value: '' - key: readability-identifier-naming.ClassCase value: lower_case - key: readability-identifier-naming.ClassConstantCase value: aNy_CasE - key: readability-identifier-naming.ClassConstantPrefix value: '' - key: readability-identifier-naming.ClassConstantSuffix value: '' - key: readability-identifier-naming.ClassMemberCase value: lower_case - key: readability-identifier-naming.ClassMemberPrefix value: '' - key: readability-identifier-naming.ClassMemberSuffix value: '_' - key: readability-identifier-naming.ClassMethodCase value: lower_case - key: readability-identifier-naming.ClassMethodPrefix value: '' - key: readability-identifier-naming.ClassMethodSuffix value: '' - key: readability-identifier-naming.ClassPrefix value: '' - key: readability-identifier-naming.ClassSuffix value: '' - key: readability-identifier-naming.ConstantCase value: aNy_CasE - key: readability-identifier-naming.ConstantMemberCase value: aNy_CasE - key: readability-identifier-naming.ConstantMemberPrefix value: '' - key: readability-identifier-naming.ConstantMemberSuffix value: '' - key: readability-identifier-naming.ConstantParameterCase value: aNy_CasE - key: readability-identifier-naming.ConstantParameterPrefix value: '' - key: readability-identifier-naming.ConstantParameterSuffix value: '' - key: readability-identifier-naming.ConstantPrefix value: '' - key: readability-identifier-naming.ConstantSuffix value: '' - key: readability-identifier-naming.ConstexprFunctionCase value: aNy_CasE - key: readability-identifier-naming.ConstexprFunctionPrefix value: '' - key: readability-identifier-naming.ConstexprFunctionSuffix value: '' - key: readability-identifier-naming.ConstexprMethodCase value: aNy_CasE - key: readability-identifier-naming.ConstexprMethodPrefix value: '' - key: readability-identifier-naming.ConstexprMethodSuffix value: '' - key: readability-identifier-naming.ConstexprVariableCase value: aNy_CasE - key: readability-identifier-naming.ConstexprVariablePrefix value: '' - key: readability-identifier-naming.ConstexprVariableSuffix value: '' - key: readability-identifier-naming.EnumCase value: lower_case - key: readability-identifier-naming.EnumConstantCase value: aNy_CasE - key: readability-identifier-naming.EnumConstantPrefix value: '' - key: readability-identifier-naming.EnumConstantSuffix value: '' - key: readability-identifier-naming.EnumPrefix value: '' - key: readability-identifier-naming.EnumSuffix value: '' - key: readability-identifier-naming.FunctionCase value: lower_case - key: readability-identifier-naming.FunctionPrefix value: '' - key: readability-identifier-naming.FunctionSuffix value: '' - key: readability-identifier-naming.GlobalConstantCase value: aNy_CasE - key: readability-identifier-naming.GlobalConstantPrefix value: '' - key: readability-identifier-naming.GlobalConstantSuffix value: '' - key: readability-identifier-naming.GlobalFunctionCase value: lower_case - key: readability-identifier-naming.GlobalFunctionPrefix value: '' - key: readability-identifier-naming.GlobalFunctionSuffix value: '' - key: readability-identifier-naming.GlobalVariableCase value: aNy_CasE - key: readability-identifier-naming.GlobalVariablePrefix value: '' - key: readability-identifier-naming.GlobalVariableSuffix value: '' - key: readability-identifier-naming.IgnoreFailedSplit value: '0' - key: readability-identifier-naming.InlineNamespaceCase value: lower_case - key: readability-identifier-naming.InlineNamespacePrefix value: '' - key: readability-identifier-naming.InlineNamespaceSuffix value: '' - key: readability-identifier-naming.LocalConstantCase value: aNy_CasE - key: readability-identifier-naming.LocalConstantPrefix value: '' - key: readability-identifier-naming.LocalConstantSuffix value: '' - key: readability-identifier-naming.LocalVariableCase value: lower_case - key: readability-identifier-naming.LocalVariablePrefix value: '' - key: readability-identifier-naming.LocalVariableSuffix value: '' - key: readability-identifier-naming.MemberCase value: lower_case - key: readability-identifier-naming.MemberPrefix value: '' - key: readability-identifier-naming.MemberSuffix value: '_' - key: readability-identifier-naming.MethodCase value: lower_case - key: readability-identifier-naming.MethodPrefix value: '' - key: readability-identifier-naming.MethodSuffix value: '' - key: readability-identifier-naming.NamespaceCase value: lower_case - key: readability-identifier-naming.NamespacePrefix value: '' - key: readability-identifier-naming.NamespaceSuffix value: '' - key: readability-identifier-naming.ParameterCase value: lower_case - key: readability-identifier-naming.ParameterPackCase value: lower_case - key: readability-identifier-naming.ParameterPackPrefix value: '' - key: readability-identifier-naming.ParameterPackSuffix value: '' - key: readability-identifier-naming.ParameterPrefix value: '' - key: readability-identifier-naming.ParameterSuffix value: '' - key: readability-identifier-naming.PrivateMemberCase value: lower_case - key: readability-identifier-naming.PrivateMemberPrefix value: '' - key: readability-identifier-naming.PrivateMemberSuffix value: '_' - key: readability-identifier-naming.PrivateMethodCase value: lower_case - key: readability-identifier-naming.PrivateMethodPrefix value: '' - key: readability-identifier-naming.PrivateMethodSuffix value: '' - key: readability-identifier-naming.ProtectedMemberCase value: lower_case - key: readability-identifier-naming.ProtectedMemberPrefix value: '' - key: readability-identifier-naming.ProtectedMemberSuffix value: '_' - key: readability-identifier-naming.ProtectedMethodCase value: lower_case - key: readability-identifier-naming.ProtectedMethodPrefix value: '' - key: readability-identifier-naming.ProtectedMethodSuffix value: '' - key: readability-identifier-naming.PublicMemberCase value: lower_case - key: readability-identifier-naming.PublicMemberPrefix value: '' - key: readability-identifier-naming.PublicMemberSuffix value: '_' - key: readability-identifier-naming.PublicMethodCase value: lower_case - key: readability-identifier-naming.PublicMethodPrefix value: '' - key: readability-identifier-naming.PublicMethodSuffix value: '' - key: readability-identifier-naming.StaticConstantCase value: aNy_CasE - key: readability-identifier-naming.StaticConstantPrefix value: '' - key: readability-identifier-naming.StaticConstantSuffix value: '' - key: readability-identifier-naming.StaticVariableCase value: lower_case - key: readability-identifier-naming.StaticVariablePrefix value: '' - key: readability-identifier-naming.StaticVariableSuffix value: '' - key: readability-identifier-naming.StructCase value: lower_case - key: readability-identifier-naming.StructPrefix value: '' - key: readability-identifier-naming.StructSuffix value: '' - key: readability-identifier-naming.TemplateParameterCase value: CamelCase - key: readability-identifier-naming.TemplateTemplateParameterCase value: CamelCase - key: readability-identifier-naming.TypedefCase value: lower_case - key: readability-identifier-naming.TypedefPrefix value: '' - key: readability-identifier-naming.TypedefSuffix value: '' - key: readability-identifier-naming.UnionCase value: lower_case - key: readability-identifier-naming.UnionPrefix value: '' - key: readability-identifier-naming.UnionSuffix value: '' - key: readability-identifier-naming.ValueTemplateParameterCase value: CamelCase - key: readability-identifier-naming.ValueTemplateParameterPrefix value: '' - key: readability-identifier-naming.ValueTemplateParameterSuffix value: '' - key: readability-identifier-naming.VariableCase value: aNy_CasE - key: readability-identifier-naming.VariablePrefix value: '' - key: readability-identifier-naming.VariableSuffix value: '' - key: readability-identifier-naming.VirtualMethodCase value: lower_case - key: readability-identifier-naming.VirtualMethodPrefix value: '' - key: readability-identifier-naming.VirtualMethodSuffix value: '' - key: readability-simplify-boolean-expr.ChainedConditionalAssignment value: '0' - key: readability-simplify-boolean-expr.ChainedConditionalReturn value: '0' - key: performance-for-range-copy.AllowedTypes value: 'offset_ptr;ptr' - key: performance-unnecessary-value-param.AllowedTypes value: 'offset_ptr;ptr' - key: readability-identifier-naming.TypeTemplateParameterIgnoredRegexp value: 'expr-type' - key: readability-identifier-naming.TemplateParameterIgnoredRegexp value: 'expr-type' - key: readability-identifier-naming.TypeTemplateParameterIgnoredRegexp value: 'expr-type' - key: readability-identifier-naming.TemplateTemplateParameterIgnoredRegexp value: 'expr-type' - key: readability-identifier-naming.ValueTemplateParameterIgnoredRegexp value: 'expr-type' ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ master ] pull_request: branches: [ master ] release: types: - published concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: openapi-validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install Dependencies run: npm install @openapitools/openapi-generator-cli -g - name: OpenAPI Lint run: openapi-generator-cli validate -i openapi.yaml ui: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - uses: pnpm/action-setup@v4 with: version: 10 - name: Install Dependencies working-directory: ui run: pnpm install - name: Build working-directory: ui run: pnpm -r build - name: Code Lint working-directory: ui run: pnpm run lint - name: Svelte Check working-directory: ui run: pnpm run check formatting: runs-on: ubuntu-latest container: ghcr.io/motis-project/docker-cpp-build steps: - uses: actions/checkout@v4 - name: Format files run: | find include src test \ -type f -a \( -name "*.cc" -o -name "*.h" -o -name ".cuh" -o -name ".cu" \) \ -print0 | xargs -0 clang-format-21 -i - name: Check for differences run: | git config --global --add safe.directory `pwd` git status --porcelain git status --porcelain | xargs -I {} -0 test -z \"{}\" msvc: runs-on: [ self-hosted, windows, x64 ] strategy: fail-fast: false matrix: config: - mode: Debug - mode: Release env: CXX: cl.exe CC: cl.exe BUILDCACHE_COMPRESS: true BUILDCACHE_DIRECT_MODE: true BUILDCACHE_ACCURACY: SLOPPY # not suitable for coverage/debugging BUILDCACHE_DIR: ${{ github.workspace }}/.buildcache BUILDCACHE_LUA_PATH: ${{ github.workspace }}/tools BUILDCACHE_MAX_CACHE_SIZE: 1073741824 CLICOLOR_FORCE: 1 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - uses: pnpm/action-setup@v4 with: version: 10 dest: "~/setup-pnpm-${{ matrix.config.mode }}" - uses: ilammy/msvc-dev-cmd@v1 # ==== RESTORE CACHE ==== - name: Restore buildcache Cache run: | $buildcachePath = "${{ runner.tool_cache }}\${{ github.event.repository.name }}\buildcache-${{ matrix.config.mode }}" New-Item -ItemType Directory -Force -Path $buildcachePath New-Item -Path ${{ github.workspace }}/.buildcache -ItemType SymbolicLink -Value $buildcachePath - name: Restore Dependencies Cache run: | $depsPath = "${{ runner.tool_cache }}\${{ github.event.repository.name }}\deps" New-Item -ItemType Directory -Force -Path $depsPath New-Item -Path ${{ github.workspace }}\deps\ -ItemType SymbolicLink -Value $depsPath - name: Build run: | cmake ` -GNinja -S . -B build ` -DCMAKE_BUILD_TYPE=${{ matrix.config.mode }} ` -DMOTIS_MIMALLOC=ON .\build\buildcache\bin\buildcache.exe -z cmake --build build --target motis motis-test motis-web-ui $CompilerExitCode = $LastExitCode Copy-Item ${env:VCToolsRedistDir}x64\Microsoft.VC143.CRT\*.dll .\build\ .\build\buildcache\bin\buildcache.exe -s exit $CompilerExitCode # ==== TESTS ==== - name: Run Tests run: .\build\motis-test.exe # ==== DISTRIBUTION ==== - name: Move Profiles if: matrix.config.mode == 'Release' run: | mkdir dist Copy-Item .\deps\tiles\profile dist\tiles-profiles -Recurse mv .\build\motis.exe dist mv .\build\*.dll dist mv .\ui\build dist\ui cd dist 7z a motis-windows.zip * mv motis-windows.zip .. - name: Upload Distribution if: matrix.config.mode == 'Release' uses: actions/upload-artifact@v4 with: name: motis-windows path: dist # ==== RELEASE ==== - name: Upload Release if: github.event.action == 'published' && matrix.config.mode == 'Release' uses: actions/upload-release-asset@v1.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: ./motis-windows.zip asset_name: motis-windows.zip asset_content_type: application/zip macos: runs-on: macos-latest env: BUILDCACHE_COMPRESS: true BUILDCACHE_DIRECT_MODE: true BUILDCACHE_ACCURACY: SLOPPY BUILDCACHE_LUA_PATH: ${{ github.workspace }}/tools BUILDCACHE_DIR: ${{ github.workspace }}/.buildcache UBSAN_OPTIONS: halt_on_error=1:abort_on_error=1 ASAN_OPTIONS: alloc_dealloc_mismatch=0 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: 16.2 - uses: pnpm/action-setup@v4 with: version: 10 - name: Install ninja run: brew install ninja # ==== RESTORE CACHE ==== - name: Restore buildcache Cache uses: actions/cache/restore@v4 id: restore-buildcache with: path: ${{ github.workspace }}/.buildcache key: buildcache-${{ hashFiles('.pkg') }}-${{ hashFiles('**/*.h') }}-${{ hashFiles('**/*.cc') }} restore-keys: | buildcache-${{ hashFiles('.pkg') }}-${{ hashFiles('**/*.h') }}- buildcache-${{ hashFiles('.pkg') }}- buildcache- - name: Dependencies Cache uses: actions/cache/restore@v4 id: restore-deps with: path: ${{ github.workspace }}/deps key: deps-${{ hashFiles('.pkg') }} restore-keys: deps- # ==== BUILD ==== - name: CMake run: cmake -G Ninja -S . -B build --preset=macos-arm64 - name: Build run: | ./build/buildcache/bin/buildcache -z cmake --build build --target motis motis-test motis-web-ui ./build/buildcache/bin/buildcache -s # ==== TESTS ==== - name: Run Tests if: matrix.config.tests == 'On' run: build/motis-test # ==== SAVE CACHE ==== - name: Save buildcache cache if: always() uses: actions/cache/save@v4 with: path: ${{ github.workspace }}/.buildcache key: ${{ steps.restore-buildcache.outputs.cache-primary-key }} - name: Save deps cache if: always() uses: actions/cache/save@v4 with: path: ${{ github.workspace }}/deps key: ${{ steps.restore-deps.outputs.cache-primary-key }} # ==== DISTRIBUTION ==== - name: Create Distribution run: | mkdir motis mv build/motis motis/motis mv ui/build motis/ui cp -r deps/tiles/profile motis/tiles-profiles tar -C ./motis -cjf motis-macos-arm64.tar.bz2 ./motis ./tiles-profiles ./ui - name: Upload Distribution uses: actions/upload-artifact@v4 with: name: motis-macos-arm64 path: motis-macos-arm64.tar.bz2 # ==== RELEASE ==== - name: Upload Release if: github.event.action == 'published' uses: actions/upload-release-asset@v1.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: ./motis-macos-arm64.tar.bz2 asset_name: motis-macos-arm64.tar.bz2 asset_content_type: application/x-tar linux: runs-on: [ self-hosted, linux, x64, '${{ matrix.config.preset }}' ] container: image: ghcr.io/motis-project/docker-cpp-build volumes: - ${{ github.event.repository.name }}-${{ matrix.config.preset }}-deps:/deps - ${{ github.event.repository.name }}-${{ matrix.config.preset }}-buildcache:/buildcache strategy: fail-fast: false matrix: config: - preset: linux-amd64-release artifact: linux-amd64 - preset: linux-arm64-release artifact: linux-arm64 - preset: linux-sanitizer - preset: linux-debug emulator: valgrind --suppressions=deps/osr/docs/tbb.supp --suppressions=deps/osr/docs/pthread.supp --suppressions=tools/suppress.txt --leak-check=full --gen-suppressions=all --error-exitcode=1 env: BUILDCACHE_DIR: /buildcache BUILDCACHE_DIRECT_MODE: true BUILDCACHE_MAX_CACHE_SIZE: 26843545600 BUILDCACHE_LUA_PATH: ${{ github.workspace }}/tools UBSAN_OPTIONS: halt_on_error=1:abort_on_error=1 ASAN_OPTIONS: alloc_dealloc_mismatch=0 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: version: 10 - name: Get deps run: ln -s /deps deps - name: CMake run: | git config --global --add safe.directory `pwd` cmake -G Ninja -S . -B build --preset=${{ matrix.config.preset }} # ==== BUILD ==== - name: Build run: | buildcache -z cmake --build build --target motis motis-test motis-web-ui buildcache -s # ==== TESTS ==== - name: Run Integration Tests if: matrix.config.preset != 'linux-arm64-release' run: ${{ matrix.config.emulator }} build/motis-test # ==== FULL DATASET TEST ==== - name: Test Full Dataset if: matrix.config.preset != 'linux-debug' && matrix.config.preset != 'linux-arm64-release' run: | ln -s deps/tiles/profile tiles-profiles wget -N https://github.com/motis-project/test-data/raw/aachen/aachen.osm.pbf wget -N https://github.com/motis-project/test-data/raw/aachen/AVV_GTFS_Masten_mit_SPNV.zip ${{ matrix.config.emulator }} ./build/motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip yq -Y -i '.timetable *= { "route_shapes": { "missing_shapes": true, "replace_shapes": true, "clasz": { "COACH": false } } }' config.yml ${{ matrix.config.emulator }} ./build/motis import ${{ matrix.config.emulator }} ./build/motis generate -n 10 ${{ matrix.config.emulator }} ./build/motis batch ${{ matrix.config.emulator }} ./build/motis compare -q queries.txt -r responses.txt responses.txt - name: Test bin_ver compatibility with previous release if: matrix.config.preset == 'linux-amd64-release' && github.event.action != 'published' run: | wget -N https://github.com/motis-project/motis/releases/latest/download/motis-linux-amd64.tar.bz2 tar -xjf motis-linux-amd64.tar.bz2 ./motis ${{ matrix.config.emulator }} ./motis import ${{ matrix.config.emulator }} ./build/motis import rm ./motis # ==== DISTRIBUTION ==== - name: Create Distribution if: matrix.config.artifact run: | mkdir motis mv build/motis motis/motis mv ui/build motis/ui cp -r deps/tiles/profile motis/tiles-profiles tar -C ./motis -cjf motis-${{ matrix.config.artifact }}.tar.bz2 ./motis ./tiles-profiles ./ui - name: Upload Distribution if: matrix.config.artifact uses: actions/upload-artifact@v4 with: name: motis-${{ matrix.config.artifact }} path: motis-${{ matrix.config.artifact }}.tar.bz2 # ==== RELEASE ==== - name: Upload Release if: github.event.action == 'published' && matrix.config.artifact uses: actions/upload-release-asset@v1.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: ./motis-${{ matrix.config.artifact }}.tar.bz2 asset_name: motis-${{ matrix.config.artifact }}.tar.bz2 asset_content_type: application/x- docker: if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} runs-on: ubuntu-latest needs: linux steps: - uses: actions/checkout@v4 - name: Download artifacts uses: actions/download-artifact@v4 - name: Docker setup-buildx uses: docker/setup-buildx-action@v3 with: install: true - name: Docker Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=edge - name: Docker build and push uses: docker/build-push-action@v5 with: push: true context: . tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 publish-motis-client: if: github.event.action == 'published' runs-on: ubuntu-latest needs: linux permissions: contents: read packages: write id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - uses: pnpm/action-setup@v4 with: version: 10 # for OIDC-based publishing to npm - name: setup npm v11 run: npm install -g npm@11 # this will override version 2.0.0 from package.json to the current git tag version - run: npm version from-git --no-git-tag-version working-directory: ui/api - run: npm ci working-directory: ui/api - run: pnpm run build working-directory: ui/api - run: npm publish --provenance --access public working-directory: ui/api ================================================ FILE: .gitignore ================================================ /tiles-profiles /*build* /.pkg.mutex /.clang-tidy /.idea .vscode/ .DS_Store /deps *.zip *.bin *.pbf *.csv /osr /delfi /test/test_case_osr /fasta.json /adr.cista* /data /test/data* dist/ fasta.json config.yaml config.yml config.ini ================================================ FILE: .pkg ================================================ [nigiri] url=git@github.com:motis-project/nigiri.git branch=master commit=8b07193adf152f258ccfa207f922579d0a0e2195 [cista] url=git@github.com:felixguendling/cista.git branch=master commit=10abc43279bd99586d3433ea3b419727f46b8dd9 [osr] url=git@github.com:motis-project/osr.git branch=master commit=78ed3a45e5b543cf4a266df005faca5c2fad8ed2 [utl] url=git@github.com:motis-project/utl.git branch=master commit=d9930c90e440df761c8c4916c36072de9dd00f49 [adr] url=git@github.com:triptix-tech/adr.git branch=master commit=64ff88efc22622a79d38c7a7e101d4a86e906a1e [googletest] url=git@github.com:motis-project/googletest.git branch=master commit=7b64fca6ea0833628d6f86255a81424365f7cc0c [net] url=git@github.com:motis-project/net.git branch=master commit=bb00cafad46dd4ea4064b9651d982f31b9853c19 [openapi-cpp] url=git@github.com:triptix-tech/openapi-cpp.git branch=master commit=1e1e5bee6a3a73270595196b2aa7f41cbe7d9214 [unordered_dense] url=git@github.com:motis-project/unordered_dense.git branch=main commit=2c7230ae7f9c30849a5b089fb4a5d11896b45dcf [reflect-cpp] url=git@github.com:motis-project/reflect-cpp.git branch=main commit=86fdcdd09a54b0f55de97110e1911d27f60e498a [tiles] url=git@github.com:motis-project/tiles.git branch=master commit=6bd71d984eb699d7160f5c448bc14d5c57ddb4b7 [boost] url=git@github.com:motis-project/boost.git branch=boost-1.89.0 commit=77467bf580d98ea06716e2931cbe3b1f28e0cd37 [tg] url=git@github.com:triptix-tech/tg.git branch=main commit=20c0f298b8ce58de29a790290f44dca7c4ecc364 [mimalloc] url=git@github.com:motis-project/mimalloc.git branch=dev3 commit=b88ce9c8fd6b7c9208a43bcdb705de9f499dbad4 [lz4] url=git@github.com:motis-project/lz4.git branch=dev commit=ff69dbd1ad10852104257d5306874a07b76f0dbd [prometheus-cpp] url=git@github.com:motis-project/prometheus-cpp.git branch=master commit=e420cd7cf3995a994220b40a36c987ac8e67c0bf [opentelemetry-cpp] url=git@github.com:motis-project/opentelemetry-cpp.git branch=main commit=57a4c01aff876e08d9d37a3dec2c9899f0606909 [ctx] url=git@github.com:motis-project/ctx.git branch=master commit=9b495bdd798520007a4f1c13e51766a26f10ef10 [geo] url=git@github.com:motis-project/geo.git branch=master commit=4a410791d3a2d77eafae917e0607051bbd4fa659 ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.20) project(motis LANGUAGES C CXX ASM) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) option(MOTIS_MIMALLOC "use mimalloc" OFF) set(MOTIS_STACKTRACE "AUTO" CACHE STRING "Enable stacktrace support (AUTO, ON, OFF)") set_property(CACHE MOTIS_STACKTRACE PROPERTY STRINGS "AUTO;ON;OFF") set(_MOTIS_STACKTRACE_ON "$,$,$>>") if (NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY) if (MOTIS_MIMALLOC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") set(protobuf_MSVC_STATIC_RUNTIME OFF) else () set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set(protobuf_MSVC_STATIC_RUNTIME ON) endif () endif () if (MOTIS_MIMALLOC) set(CISTA_USE_MIMALLOC ON) set(PPR_MIMALLOC ON) set(ADR_MIMALLOC ON) set(OSR_MIMALLOC ON) set(TILES_MIMALLOC ON) if(WIN32) set(MI_BUILD_SHARED ON) endif() endif() include(cmake/buildcache.cmake) include(cmake/pkg.cmake) if (MOTIS_MIMALLOC) if(WIN32) set(motis-mimalloc-lib mimalloc) target_link_libraries(cista INTERFACE mimalloc) else() set(motis-mimalloc-lib mimalloc-obj) target_link_libraries(cista INTERFACE mimalloc-static) endif() target_compile_definitions(cista INTERFACE CISTA_USE_MIMALLOC=1) target_compile_definitions(boost INTERFACE BOOST_ASIO_DISABLE_STD_ALIGNED_ALLOC=1) endif() # --- LINT --- option(ICC_LINT "Run clang-tidy with the compiler." OFF) if (ICC_LINT) # clang-tidy will be run on all targets defined hereafter include(cmake/clang-tidy.cmake) endif () set(CMAKE_COMPILE_WARNING_AS_ERROR ON) if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(motis-compile-options -Weverything -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-newline-eof -Wno-missing-prototypes -Wno-padded -Wno-double-promotion -Wno-undef -Wno-undefined-reinterpret-cast -Wno-float-conversion -Wno-global-constructors -Wno-exit-time-destructors -Wno-switch-enum -Wno-c99-designator -Wno-zero-as-null-pointer-constant -Wno-missing-noreturn -Wno-undefined-func-template -Wno-unsafe-buffer-usage -Wno-c++20-compat -Wno-reserved-macro-identifier -Wno-documentation-unknown-command -Wno-duplicate-enum -Wno-ctad-maybe-unsupported -Wno-unknown-pragmas -Wno-c++20-extensions -Wno-switch-default -Wno-unused-template -Wno-shadow-uncaptured-local -Wno-documentation-deprecated-sync -Wno-float-equal -Wno-deprecated-declarations -Wno-reserved-identifier -Wno-implicit-int-float-conversion -Wno-nrvo -Wno-thread-safety-negative -Wno-unused-private-field) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") set(motis-compile-options -Wall -Wextra -Wno-unknown-pragmas -Wno-deprecated-declarations) elseif (MSVC) set(motis-compile-options /bigobj # clang-21 libc++ seems not to have a special overloaded std::atomic> # check if future version support std::atomic> /D_SILENCE_CXX20_OLD_SHARED_PTR_ATOMIC_SUPPORT_DEPRECATION_WARNING ) else () set(motis-compile-options -Wall -Wextra -Wno-array-bounds -Wno-stringop-overread -Wno-mismatched-new-delete -Wno-maybe-uninitialized) endif () # --- OPENAPI --- openapi_generate(openapi.yaml motis-api motis::api) # --- LIB --- file(GLOB_RECURSE motislib-files src/*.cc) add_library(motislib ${motislib-files}) target_include_directories(motislib PUBLIC include) target_compile_features(motislib PUBLIC cxx_std_23) target_compile_options(motislib PRIVATE ${motis-compile-options}) target_link_libraries(motislib nigiri osr adr ctx boost-json motis-api reflectcpp web-server tiles pbf_sdf_fonts_res ssl crypto tg lz4_static lb web-server prometheus-cpp::core opentelemetry_trace opentelemetry_exporter_otlp_http lmdb Boost::stacktrace "$<${_MOTIS_STACKTRACE_ON}:Boost::stacktrace_from_exception>" ) # --- EXE --- execute_process( COMMAND git describe --always --tags --dirty=-dirty WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE motis-git-tag OUTPUT_STRIP_TRAILING_WHITESPACE ) file(GLOB_RECURSE motis-files exe/*.cc) add_executable(motis ${motis-files}) target_compile_features(motis PUBLIC cxx_std_23) target_compile_options(motis PRIVATE ${motis-compile-options}) set_source_files_properties(exe/main.cc PROPERTIES COMPILE_DEFINITIONS MOTIS_VERSION="${motis-git-tag}") target_link_libraries(motis motislib ianatzdb-res pbf_sdf_fonts_res-res tiles_server_res-res address_formatting_res-res ) if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") target_link_options(motis PRIVATE -Wl,-no_deduplicate) endif() # --- TEST --- add_library(motis-generated INTERFACE) target_include_directories(motis-generated INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/generated) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/test/test_dir.h.in ${CMAKE_CURRENT_BINARY_DIR}/generated/test_dir.h ) file(GLOB_RECURSE motis-test-files test/*.cc) add_executable(motis-test ${motis-test-files}) target_link_libraries(motis-test motislib gmock web-server ianatzdb-res address_formatting_res-res motis-generated) target_compile_options(motis-test PRIVATE ${motis-compile-options}) # --- TILES --- set_property( TARGET motis tiles tiles-import-library APPEND PROPERTY COMPILE_DEFINITIONS TILES_GLOBAL_PROGRESS_TRACKER=1) file (CREATE_LINK ${CMAKE_SOURCE_DIR}/deps/tiles/profile ${CMAKE_BINARY_DIR}/tiles-profiles SYMBOLIC) # --- MIMALLOC --- if (MOTIS_MIMALLOC) target_link_libraries(motis ${motis-mimalloc-lib}) target_compile_definitions(motis PUBLIC USE_MIMALLOC=1) if(WIN32) add_custom_command( TARGET motis POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy $ $ COMMENT "Copy mimalloc.dll to output directory" ) add_custom_command( TARGET motis POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/deps/mimalloc/bin/mimalloc-redirect.dll" $ COMMENT "Copy mimalloc-redirect.dll to output directory" ) add_custom_command( TARGET motis POST_BUILD COMMAND "${CMAKE_SOURCE_DIR}/deps/mimalloc/bin/minject.exe" --force --inplace $<$:--postfix=debug> $ COMMENT "Ensure mimalloc.dll is loaded first" ) add_custom_command( TARGET motis-test POST_BUILD COMMAND "${CMAKE_SOURCE_DIR}/deps/mimalloc/bin/minject.exe" --force --inplace $<$:--postfix=debug> $ COMMENT "Ensure mimalloc.dll is loaded first" ) endif() if (MSVC) target_link_options(motis PUBLIC "/include:mi_version") endif () endif() # --- UI --- add_custom_target(motis-web-ui COMMAND pnpm install && pnpm -r build WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/ui" VERBATIM ) file (CREATE_LINK ${CMAKE_SOURCE_DIR}/ui/build ${CMAKE_BINARY_DIR}/ui SYMBOLIC) foreach(t adr osr nigiri gtfsrt geo tiles tiles-import-library motis motis-api motislib) target_compile_options(${t} PUBLIC ${MOTIS_TARGET_FLAGS}) endforeach() if (MOTIS_MIMALLOC) target_compile_options(mimalloc PUBLIC ${MOTIS_TARGET_FLAGS}) endif() ================================================ FILE: CMakePresets.json ================================================ { "version": 3, "cmakeMinimumRequired": { "major": 3, "minor": 21, "patch": 0 }, "configurePresets": [ { "name": "macos-x86_64", "displayName": "MacOS x86_64 Release", "generator": "Ninja", "binaryDir": "${sourceDir}/build/macos-x86_64-release", "cacheVariables": { "BOOST_CONTEXT_ABI": "sysv", "BOOST_CONTEXT_ARCHITECTURE": "x86_64", "CMAKE_OSX_ARCHITECTURES": "x86_64", "CMAKE_CXX_FLAGS": "-stdlib=libc++", "CMAKE_BUILD_TYPE": "Release" } }, { "name": "macos-arm64", "displayName": "MacOS ARM64 Release", "generator": "Ninja", "binaryDir": "${sourceDir}/build/macos-arm64-release", "cacheVariables": { "CMAKE_OSX_ARCHITECTURES": "arm64", "CMAKE_CXX_FLAGS": "-stdlib=libc++", "BOOST_CONTEXT_ARCHITECTURE": "arm64", "BOOST_CONTEXT_ABI": "aapcs", "ENABLE_ASM": "OFF", "CMAKE_BUILD_TYPE": "Release" } }, { "name": "linux-amd64-release", "displayName": "Linux AMD64 Release", "generator": "Ninja", "binaryDir": "${sourceDir}/build/amd64-release", "toolchainFile": "/opt/x86_64-multilib-linux-musl/toolchain-amd64.cmake", "cacheVariables": { "CMAKE_EXE_LINKER_FLAGS": "-B/opt/mold", "CMAKE_BUILD_TYPE": "Release", "MOTIS_MIMALLOC": "ON" }, "environment": { "PATH": "/opt:/opt/cmake-3.26.3-linux-x86_64/bin:/opt/buildcache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } }, { "name": "linux-arm64-release", "displayName": "Linux ARM64 Release", "generator": "Ninja", "binaryDir": "${sourceDir}/build/arm64-release", "toolchainFile": "/opt/aarch64-unknown-linux-musl/toolchain-arm64.cmake", "cacheVariables": { "CMAKE_CROSSCOMPILING_EMULATOR": "qemu-aarch64-static", "CMAKE_C_FLAGS": "-mcpu=neoverse-n1", "CMAKE_CXX_FLAGS": "-mcpu=neoverse-n1", "CMAKE_EXE_LINKER_FLAGS": "-B/opt/mold", "CMAKE_BUILD_TYPE": "Release", "MOTIS_MIMALLOC": "ON" }, "environment": { "PATH": "/opt:/opt/cmake-3.26.3-linux-x86_64/bin:/opt/buildcache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } }, { "name": "linux-sanitizer", "displayName": "Linux Sanitizer", "generator": "Ninja", "binaryDir": "${sourceDir}/build/sanitizer", "cacheVariables": { "CMAKE_C_COMPILER": "clang-21", "CMAKE_CXX_COMPILER": "clang++-21", "CMAKE_EXE_LINKER_FLAGS": "-lc++abi", "CMAKE_BUILD_TYPE": "Debug", "CTX_ASAN": "ON", "CMAKE_C_FLAGS": "-fsanitize=address,undefined -fsanitize-ignorelist=${sourceDir}/tools/ubsan-suppress.txt -fno-omit-frame-pointer", "CMAKE_CXX_FLAGS": "-stdlib=libc++ -fsanitize=address,undefined -fno-omit-frame-pointer" }, "environment": { "PATH": "/opt:/opt/cmake-3.26.3-linux-x86_64/bin:/opt/buildcache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } }, { "name": "linux-debug", "displayName": "Linux Debug", "generator": "Ninja", "binaryDir": "${sourceDir}/build/debug", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_EXE_LINKER_FLAGS": "-B/opt/mold", "CTX_VALGRIND": "ON" }, "environment": { "PATH": "/opt:/opt/cmake-3.26.3-linux-x86_64/bin:/opt/buildcache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "CXX": "/usr/bin/g++-13", "CC": "/usr/bin/gcc-13" } }, { "name": "linux-relwithdebinfo", "displayName": "Linux RelWithDebInfo", "generator": "Ninja", "binaryDir": "${sourceDir}/build/relwithdebinfo", "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "CMAKE_EXE_LINKER_FLAGS": "-B/opt/mold" }, "environment": { "PATH": "/opt:/opt/cmake-3.26.3-linux-x86_64/bin:/opt/buildcache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "CXX": "/usr/bin/g++-13", "CC": "/usr/bin/gcc-13" } }, { "name": "clang-tidy", "displayName": "Clang Tidy", "generator": "Ninja", "binaryDir": "${sourceDir}/build/clang-tidy", "cacheVariables": { "CMAKE_C_COMPILER": "clang-21", "CMAKE_CXX_COMPILER": "clang++-21", "CMAKE_CXX_FLAGS": "-stdlib=libc++", "CMAKE_EXE_LINKER_FLAGS": "-lc++abi", "CMAKE_BUILD_TYPE": "Release", "ICC_LINT": "On" }, "environment": { "BUILDCACHE_LUA_PATH": "/opt/buildcache/share/lua-examples", "PATH": "/opt:/opt/cmake-3.26.3-linux-x86_64/bin:/opt/buildcache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } } ], "buildPresets": [ { "name": "macos-x86_64", "configurePreset": "macos-x86_64" }, { "name": "macos-arm64", "configurePreset": "macos-arm64" }, { "name": "linux-amd64-release", "configurePreset": "linux-amd64-release" }, { "name": "linux-arm64-release", "configurePreset": "linux-arm64-release" }, { "name": "clang-tidy", "configurePreset": "clang-tidy" }, { "name": "linux-debug", "configurePreset": "linux-debug" }, { "name": "linux-relwithdebinfo", "configurePreset": "linux-relwithdebinfo" }, { "name": "linux-sanitizer", "configurePreset": "linux-sanitizer" } ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at felix@triptix.tech. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: Dockerfile ================================================ FROM alpine:3.20 ARG TARGETARCH ADD motis-linux-$TARGETARCH/motis-linux-$TARGETARCH.tar.bz2 / RUN addgroup -S motis && adduser -S motis -G motis && \ mkdir /data && \ chown motis:motis /data EXPOSE 8080 VOLUME ["/data"] USER motis CMD ["/motis", "server", "/data"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 MOTIS Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

> [!TIP] > :sparkles: Join the international MOTIS community at [**motis:matrix.org**](https://matrix.to/#/#motis:matrix.org) MOTIS stands for **M**odular **O**pen **T**ransportation **I**nformation **S**ystem. It is an open-source software platform designed to facilitate efficient planning and routing in multi-modal transportation systems. Developed to handle *large-scale* transportation data, MOTIS integrates various modes of transport - such as walking, cycling, sharing mobility (e-scooters, bike sharing, car sharing), and public transport - to provide optimized routing solutions. MOTIS currently supports the following input formats: - (One) **OpenStreetMap `osm.pbf`** file for the street network, addresses, indoor-routing, etc. - (Multiple) **GTFS** (including GTFS Flex and GTFS Fares v2) feeds for static timetables - (Multiple) **GTFS-RT** feeds for real-time updates (delays, cancellations, track changes, service alerts) - (Multiple) **GBFS** feeds for sharing mobility *Working on (funded by [NLnet](https://nlnet.nl/project/MOTIS/))*: NeTEx and SIRI MOTIS provides an easy-to-use **REST API** (JSON via HTTP) with an [**OpenAPI specification**](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/motis-project/motis/refs/heads/master/openapi.yaml) ([source](openapi.yaml)) that allows you to generate clients for your favorite programming language. You may also directly use the pre-generated [JS client](https://www.npmjs.com/package/@motis-project/motis-client). Some more available client libraries are listed [over at Transitous](https://transitous.org/api/). Also checkout [**Transitous**](https://transitous.org), which operates a MOTIS instance with global coverage (as far as available) at [api.transitous.org](https://api.transitous.org). Please make sure to read the [Usage Policy](https://transitous.org/api/) before integrating this endpoint into your app. # Features > [!NOTE] > :rocket: MOTIS is optimized for **high performance** with **low memory usage**. > > This enables _planet-sized_ deployments on affordable hardware. MOTIS is a swiss army knife for mobility and comes with all features you need for a next generation mobility platform: - **routing**: one mode walking, bike, car, sharing mobility / combined modes - **geocoding**: multi-language address and stop name completion with fuzzy string matching and resolution to geo coordinates - **reverse geocoding**: resolving geo coordinates to the closest address - **tile server**: background map tiles MOTIS uses efficient traffic day bitsets that allows efficient loading of **full year timetables**! Loading one year of timetable doesn't take much more RAM than loading one month. Features can be turned on and off as needed. # Quick Start - Create a folder with the following files. - Download MOTIS from the [latest release](https://github.com/motis-project/motis/releases) and extract the archive. - Download a OpenStreetMap dataset as `osm.pbf` (e.g. from [Geofabrik](https://download.geofabrik.de/)) and place it in the folder - Download one or more GTFS datasets and place them in the folder ```bash ./motis config my.osm.pbf gtfs.zip # generates a minimal config.yml ./motis import # preprocesses data ./motis server # starts a HTTP server on port 8080 ``` This will preprocess the input files and create a `data` folder. After that, it will start a server. > [!IMPORTANT] > Ensure a valid timetable is used. If the timetable is outdated, it will not contain any trips to consider for upcoming dates. This script will execute the steps described above for a small dataset for the city of Aachen, Germany: **Linux / macOS** ```bash # set TARGET to linux-arm64, macos-arm64, ... to fit your setup # see release list for supported platforms TARGET="linux-amd64" wget https://github.com/motis-project/motis/releases/latest/download/motis-${TARGET}.tar.bz2 tar xf motis-${TARGET}.tar.bz2 wget https://github.com/motis-project/test-data/raw/aachen/aachen.osm.pbf wget https://opendata.avv.de/current_GTFS/AVV_GTFS_Masten_mit_SPNV.zip ./motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip ./motis import ./motis server ``` **Windows** ```pwsh Invoke-WebRequest https://github.com/motis-project/motis/releases/latest/download/motis-windows.zip -OutFile motis-windows.zip Expand-Archive motis-windows.zip Invoke-WebRequest https://github.com/motis-project/test-data/archive/refs/heads/aachen.zip -OutFile aachen.zip Expand-Archive aachen.zip ./motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip ./motis import ./motis server ``` # Documentation ## Developer Setup Build MOTIS from source: - [for Linux](docs/linux-dev-setup.md) - [for Windows](docs/windows-dev-setup.md) - [for macOS](docs/macos-dev-setup.md) Set up a server using your build: - [for Linux](docs/dev-setup-server.md) MOTIS uses [pkg](https://github.com/motis-project/pkg) for dependency management. See its [README](https://github.com/motis-project/pkg/blob/master/README.md) for how to work with it. ## Configuration - [Advanced Setups](docs/setup.md) ================================================ FILE: cmake/buildcache.cmake ================================================ option(NO_BUILDCACHE "Disable build caching using buildcache" Off) # PDB debug information is not supported by buildcache. # Store debug info in the object files. if (DEFINED ENV{GITHUB_ACTIONS}) string(REPLACE "/Zi" "" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") string(REPLACE "/Zi" "" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}") string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") else () string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}") string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") endif () set(buildcache-bin ${CMAKE_CURRENT_BINARY_DIR}/buildcache/bin/buildcache) get_property(rule-launch-set GLOBAL PROPERTY RULE_LAUNCH_COMPILE SET) if (NO_BUILDCACHE) message(STATUS "NO_BUILDCACHE set, buildcache disabled") elseif (rule-launch-set) message(STATUS "Global property RULE_LAUNCH_COMPILE already set - skipping buildcache") else () find_program(buildcache_program buildcache HINTS ${CMAKE_CURRENT_BINARY_DIR}/buildcache/bin) if (buildcache_program) message(STATUS "Using buildcache: ${buildcache_program}") set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${buildcache_program}") else () message(STATUS "buildcache not found - downloading") if (APPLE) set(buildcache-archive "buildcache-macos.zip") elseif (UNIX AND ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "aarch64") set(buildcache-archive "buildcache-linux-arm64.tar.gz") elseif (UNIX) set(buildcache-archive "buildcache-linux-amd64.tar.gz") elseif (WIN32) set(buildcache-archive "buildcache-windows.zip") else () message(FATAL "Error: NO_BUILDCACHE was not set but buildcache was not in path and system OS detection failed") endif () set(buildcache-url "https://gitlab.com/bits-n-bites/buildcache/-/releases/v0.31.7/downloads/${buildcache-archive}") message(STATUS "Downloading buildcache binary from ${buildcache-url}") file(DOWNLOAD "${buildcache-url}" ${CMAKE_CURRENT_BINARY_DIR}/${buildcache-archive}) execute_process( COMMAND ${CMAKE_COMMAND} -E tar xf ${CMAKE_CURRENT_BINARY_DIR}/${buildcache-archive} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) message(STATUS "using buildcache: ${buildcache-bin}") set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${buildcache-bin}) endif () endif () ================================================ FILE: cmake/clang-tidy.cmake ================================================ if (CMake_SOURCE_DIR STREQUAL CMake_BINARY_DIR) message(FATAL_ERROR "CMake_RUN_CLANG_TIDY requires an out-of-source build!") endif () file(RELATIVE_PATH RELATIVE_SOURCE_DIR ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}) if (ICC_CLANG_TIDY_COMMAND) set(CLANG_TIDY_COMMAND "${ICC_CLANG_TIDY_COMMAND}") else () find_program(CLANG_TIDY_COMMAND NAMES clang-tidy-21) endif () if (NOT CLANG_TIDY_COMMAND) message(FATAL_ERROR "CMake_RUN_CLANG_TIDY is ON but clang-tidy is not found!") endif () set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}") file(SHA1 ${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy.in clang_tidy_sha1) set(CLANG_TIDY_DEFINITIONS "CLANG_TIDY_SHA1=${clang_tidy_sha1}") unset(clang_tidy_sha1) configure_file(.clang-tidy.in ${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy) ================================================ FILE: cmake/pkg.cmake ================================================ if (NOT DEFINED PROJECT_IS_TOP_LEVEL OR PROJECT_IS_TOP_LEVEL) find_program(pkg-bin pkg HINTS /opt/pkg) if (pkg-bin) message(STATUS "found pkg ${pkg-bin}") else () set(pkg-bin "${CMAKE_BINARY_DIR}/dl/pkg") if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "aarch64") set(pkg-url "pkg-linux-arm64") elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(pkg-url "pkg") elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") set(pkg-url "pkg.exe") elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") set(pkg-url "pkgosx") else () message(STATUS "Not downloading pkg tool. Using pkg from PATH.") set(pkg-bin "pkg") endif () if (pkg-url) if (NOT EXISTS ${pkg-bin}) message(STATUS "Downloading pkg binary from https://github.com/motis-project/pkg/releases/latest/download/${pkg-url}") file(DOWNLOAD "https://github.com/motis-project/pkg/releases/latest/download/${pkg-url}" ${pkg-bin}) if (UNIX) execute_process(COMMAND chmod +x ${pkg-bin}) endif () else () message(STATUS "Pkg binary located in project.") endif () endif () endif () if (DEFINED ENV{GITHUB_ACTIONS}) message(STATUS "${pkg-bin} -l -h -f") execute_process( COMMAND ${pkg-bin} -l -h -f WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE pkg-result ) else () message(STATUS "${pkg-bin} -l") execute_process( COMMAND ${pkg-bin} -l WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE pkg-result ) endif () if (NOT pkg-result EQUAL 0) message(FATAL_ERROR "pkg failed: ${pkg-result}") endif () if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/deps") add_subdirectory(deps) endif () set_property( DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/.pkg" ) endif () ================================================ FILE: docs/STYLE.md ================================================ # MOTIS C++ Style # Preamble Beware that these rules only apply to MOTIS C++ and are very opinionated. C++ has a big diversity of programming styles from "C with classes" to "Modern C++". A lot of codebases have specific rules that make sense in this specific context (e.g. embedded programming, gaming, Google search, etc.) and therefore different guidelines. Over the years we learned that the rules described here are a good fit for this specific project. So in general our goals are: - We want high-level, maintainable C++ code by default, not "high level assembly" - but: don’t use features just because you can (like template meta programming, etc.) # Style - Header names: **`*.h`**, Implementation names: **`*.cc`** - Don’t use include guards (`#ifndef #define #endif`), use **`#pragma once`** - Consistently use **`struct`** instead of `class` - default visibility: public (which is what we need → no getter / setter) - you don’t need to write a constructor for 1-line initialization - Always use ++i instead of i++ if it makes no difference for the program logic: `for (auto i = 0U; i < 10; ++i) { … }` - Don't `using namespace std;` - Don’t use `NULL` or `0`, use **nullptr** instead - Don’t write `const auto&`, write **`auto const&`** - Don’t write `const char*`, write **`char const*`** # Case - Everything **`snake_case`** (as in the C++ Standard Library) - Template parameters **`PascalCase`** (as in the C++ Standard Library) - Constants **`kPascalCase`** (as in the Google C++ Styleguide), not `UPPER_CASE` to prevent collisions with macro names - Postfix **`member_variables_`** with an underscore to improve code readability when reading code without an IDE ```cpp constexpr auto kMyConstant = 3.141; template struct my_class : public my_parent { void member_fn(std::string const& fn_param) const override { auto const local_cvar = abc(); auto local_var = def(); } int my_field_; }; ``` # Includes - Include only what you use (but everything you use!) - Group includes: - for `.cc` files: first its own `.h` file - Standard headers with `<...>` syntax - C headers (use `` instead of ``, etc.) - C++ standard library headers (e.g. ``) - Non-standard headers with `"..."` syntax - generic to specific = boost libraries, then more and more specific - last: project includes - if available: local includes `"./test_util.h"` from the local folder (only done for tests) - Do not repeat include files from your own header file - Repeat everything else - even it's transitiveley included already through other headers. The include might be removed from the header you include which leads broken compilation. Try to make the compilation as robust as possible. Example include files for `message.cc`: ```cpp #include "motis/module/message.h" #include #include #include "boost/asio.hpp" #include "flatbuffers/idl.h" #include "flatbuffers/util.h" #include "motis/core/common/logging.h" ``` # Simplify Code: Predicate Usage ```cpp // bad if (is_valid()) { set_param(false); } else { set_param(true); } // bad set_param(is_valid() ? false : true); // good set_param(!is_valid()); ``` # Always use Braces ```cpp // bad for (auto i = 0u; i < 10; ++i) if (is_valid()) return get_a(); else count_b(); // good for (auto i = 0u; i < 10; ++i) { if (is_valid()) { return get_a(); } else { count_b(); } } ``` # Use Short Variable Names Only use shortened version of the variable name if it's still obvious what the variable holds. - Index = `idx` - Input = `in` - Output = `out` - Request = `req` - Response = `res` - Initialization = `init` - ... etc. If the context in which the variable is used is short, you can make variable names even shorter. For example `for (auto const& e : events) { /* ... */ }` or `auto const& b = get_buffer()`. Don't use `lhs` and `rhs` - for comparison with `friend bool operator==`. Use `a` and `b`. # Signatures in Headers Omit information that's not needed for a forward declaration. ```cpp void* get_memory(my_memory_manager& memory_manager); // bad void* get_memory(my_memory_manager&); // good // const for value parameters is not needed in headers void calc_mask(bool const, bool const, bool const, bool const); // bad void calc_mask(bool local_traffic, // slightly less bad bool long_distance_traffic, bool local_stations, bool long_distance_stations); void calc_mask(mask_options); // good ``` # Low Indentation Try to keep indentation at a minimum by handling cases one by one and bailing out early. Example: Bad: ```cpp int main(int argc, char** argv) { if (argc > 1) { for (int i = 0; i < argc; ++i) { if (std::strcmp("hello", argv[i]) == 0) { /* ... 100 lines of code ... */ } } } } ``` Good: ```cpp int main(int argc, char** argv) { if (argc <= 1) { return 0; } for (int i = 0; i < argc; ++i) { if (std::strcmp("hello", argv[i]) != 0) { continue; } /* ... 100 lines of code ... */ } } ``` # Function Length / File Length Functions should have one task only. If they grow over ~50 lines of code, please check if they could be split into several functions to improve readability. But: don't split just randomly to not go over some arbitrary lines of code limit. - Better: split earlier if it makes sense! Files are free! (more than one responsibility) - Split later i.e. if you want to keep one block of logic without interruption (easier to understand) # Pointers Read C++ data types from right to left: **`int const* const`** - `const` (read only) pointer (address can't be modified) - to `const int` (int value at address can't be modified) **int const&** - reference - on a const `int` value (read only) **auto const&** - reference - on a value (type deduced by the compiler) # Use RAII Whenever possible use RAII to manage resource like memory (`std::unique_ptr`, `std::shared_ptr`, etc.), files (`std::fstream`), network sockets (Boost Asio), etc. This means we do not want `new` or `delete` - except for placement new or placement delete in some very specific cases. # Use `utl` Library If there is no tool available in the C++ Standard Library please check first if we already have something in our [utl](https://github.com/motis-project/utl) library. # Use `strong` types Use `cista::strong` to define types, that cannot be converted implicitly. Using a `strong` type will ensure, that parameters cannot be mismatched, unlike `int` or `std::size_t`. This also makes function parameters clearer. # `const` Make everything (variables, loop variables, member functions, etc.) as `const` as possible. This indicates thread-safety (as long as only `const` methods are used) and helps to catch bugs when our mental model doesn't match the reality (the compiler will tell us). # Initialization Use [Aggregate Initialization](https://en.cppreference.com/w/cpp/language/aggregate_initialization) if possible. This also applies to member variables. A big advantage is that it doesn't allow implicit type conversions. # Namespaces Rename long namespace names instead of importing them completely. ```cpp using boost::program_options; // bad namespace po = boost::program_options; // good ``` This way we still know where functions come from when reading code. It becomes hard to know where a function came from when several large namespaces are completely imported. Don't alias or import namespaces in header files. # AAA-Style Use [Almost Always Auto (AAA)](https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/) style if possible. - Program against interfaces - Abstraction - Less typing Example: `for (auto const& el : c())` No client code change if - c returns another collection type (i.e. set instead of vector) - the element type changes but still has a compatible interface # No Raw Loops It takes time to understand a raw for loop: ```cpp for (int i = -1; i <= 9; i += 2) { if (i % 2 == 0) { continue; } if (i > 5 && i % 2 == 1) { break; } printf("%d\n", i/3); } ``` - Raw for loops can - do crazy things - be boring (can often be expressed with a standard library algorithm!!) - Find an element loop → `std::find`, `std::lower_bound`, ... - Check each element loop → `std::all_of`, `std::none_of`, `std::any_of` - Conversion loop → `std::transform`, `utl::to_vec` - Counting: `std::count_if`, `std::accumulate` - Sorting: `std::sort`, `std::nth_element`, `std::is_sorted` - Logic: `std::all_of`, `std::any_of` - Iterating multiple elements at once: `utl::zip`, `utl::pairwise`, `utl::nwise` - Erasing elements: `utl::erase_if`, `utl::erase_duplicates` - etc. Hint: `utl` provides a cleaner interface wrapping `std::` functions for collections so you don't have to call `begin` and `end` all the time! Benefits: - Function name tells the reader of your code already what it does! - Standard library implementation does not contain errors and is performant! Alternative (if no function in the standard or `utl` helps): - Use range based for loop if there's no named function: `for (auto const& el : collection) { .. }` # Comparators Either use - Preferred: mark the operator you need `= default;` - If that doesn't do the job you can check `CISTA_FRIEND_COMPARABLE` - If you want to be selective and only compare a subset of member variables: `std::tie(a_, b_) == std::tie(a_, b_)` # Set/Map vs Vector Our go-to data structure is `std::vector`. (Hash-)maps and (hash-)sets are very expensive. Never use `std::unordered_map`. We have better alternatives in all projects (e.g. unordered_dense). ## `vecvec` and `vector_map` - Use `vector_map` for mappings with a `strong` key type and a continuous domain. - Prefer using `vecvec` instead of `vector>`, as data is stored and accessed more efficient. To store data, that may appear in any order, you may consider `paged_vecvec` instead. # Tooling - Always develop with Address Sanitizer (ASan) and Undefined Behaviour Sanitizer (UBSan) enabled if performance allows it (it's usually worth it to use small data sets to be able to develop with sanitizers enabled!): `CXXFLAGS=-fno-omit-frame-pointer -fsanitize=address,undefined`. - **Notice**: Some checks can cause false positive and should be disabled if necessary (compare `ci.yml`). Example: `ASAN_OPTIONS=alloc_dealloc_mismatch=0` - Check your code with `valgrind`. # Spirit - No deep inheritance hierarchies (no "enterprise" code) - Don't write getters / setters for member variables: just make them public (which is the default for `struct` - remember: always use structs) - Don't introduce a new variable for every value if it gets used only one time and the variable doesn't tell the reader any important information (-> inline variables). - No GoF "design patterns" (Factory, Visitor, ...) if there is a simpler solution (there's always a simpler solution) - Function / struct length: - it should be possible to understand every function by shortly looking at it - hints where to split: - single responsibility - short enough to be reusable in another context - Don’t write “extensible” code that cares for functionality you might need at some point in the future. Just solve the problem at hand. - Build the **smallest and simplest** solution possible that solves your problem - Use abstractions to avoid thinking about details: helps to keep functions short - Comment only the tricky / hacky pieces of your code (there should not be too many comments, otherwise your code is bad) - Instead of comments use good (but short!) names for variables and functions - Less code = less maintenance, less places for bugs, easier to understand - Write robust code: `utl::verify()` assumptions about input data ================================================ FILE: docs/dev-setup-server.md ================================================ # Setting up a server from a development build 1. Build `motis`. Refer to the respective documentation if necessary: - [for Linux](linux-dev-setup.md) - [for Windows](windows-dev-setup.md) - [for macOS](macos-dev-setup.md) 2. Build the UI: ```shell motis$ cd ui motis/ui$ pnpm install motis/ui$ pnpm -r build ``` 3. Move the UI build into the build folder of `motis`: ```shell motis$ mv ui/build build/ui ``` 4. Copy the tiles profiles to the `motis` build folder: ```shell motis$ cp -r deps/tiles/profile build/tiles-profiles ``` 5. Download OpenStreetMap and timetable datasets and place them in the build folder of `motis`: ```shell motis/build$ wget https://github.com/motis-project/test-data/raw/aachen/aachen.osm.pbf motis/build$ wget https://opendata.avv.de/current_GTFS/AVV_GTFS_Masten_mit_SPNV.zip ``` 6. Run `motis config` on the downloaded datasets to create a config file: ```shell motis/build$ ./motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip ``` 7. Run `motis import` and then start the server using `motis server`: ```shell motis/build$ ./motis import motis/build$ ./motis server ``` 8. Open `localhost:8080` in a browser to see if everything is working. ================================================ FILE: docs/elevation-setup.md ================================================ # Setting up elevation tiles This page explains how to set up elevation tiles, that are required for elevation profiles. For performance reasons, all tile data must be stored uncompressed. This will require roughly `350 GB` for the full SRTMGL1 data set. After the import, the elevation data requires less disk space than the `way_osm_nodes_index.bin` and `way_osm_nodes_data.bin` files. ## Data formats Elevation data must be provided in a tile data format, which has been implemented in and is supported by `osr`. Currently supported formats: - **SRTMHGT**: Format used for SRTMGL1 data - **EHdr**, also known _BIL format_: Format, that has been used for SRTM data in the past For more details about these formats and a list of additional raster drivers also see https://gdal.org/en/stable/drivers/raster/ ## Set up SRTM files The SRTMGL1 data is provided by the LP DAAC at https://lpdaac.usgs.gov/products/srtmgl1v003/ Notice that the website will be moving by June 17, 2025. The data should then be available at the new website: https://www.earthdata.nasa.gov/centers/lp-daac ### Data download All HGT data tiles can be downloaded using _NASA Earthdata Search_: https://search.earthdata.nasa.gov/search?q=C2763266360-LPCLOUD Downloading tiles requires a free account. 1. Log in and create an account if necessary 1. Enter https://search.earthdata.nasa.gov/search?q=C2763266360-LPCLOUD and select the _NASA Shuttle Radar Topography Mission Global 1 arc second V003_ data set 1. Select all tiles you want to download - Use can use the _Search by spatial polygon_ or _Search by spatial rectangle_ options in the bottom right to select smaller regions 1. Press _Download All_, once the tiles have been selected and continue by confirming with _Download Data_ 1. Files can now be downloaded directly (_Download Files_) or using the _Download Script_. Notice: Depending on the number of tiles to download, the server might take several minutes, before all links are created. Wait until the process is completed, as there will be missing tiles otherwise. 1. Move and extract all HGT files into a directory. Make sure the file name isn't changed, as it will be used to match coordinates, e.g. `N52E013.SRTMGL1.hgt` **Important**: HGT tiles not matching the naming convention will not be found On Unix based systems the following script can be used to extract all ZIP files into a new subdirectory `hgt`: ```sh #!/bin/sh [ -d 'hgt' ] || mkdir 'hgt' # Create directory if necessary for f in *.hgt.zip; do [ -s "${f}" ] || continue # Ensure file exists and is not empty unzip `# Always override` -o "${f}" `# Extract into directory` -d 'hgt' rm -f "${f}" # Delete compressed file done ``` ### Using the SRTM elevation data Assume the extracted HGT tiles are stored at `/srtm`. 1. Edit `config.yml`: ```yml street_routing: elevation_data_dir: /srtm ``` This replaces any existing setting `street_routing: true|false` 1. Run `motis import` to import the elevation data ## Using multiple data tile formats For global routing using elevation data, multiple data sources might be required. It's therefore possible to use tiles with different formats simultaneously. 1. Create a new directory `` 1. For each data source create a new sub directory 1. Move all elevation data files into the corresponding directories 1. Set `elevation_data_dir: ` in `config.yml` Ensure the directory only contains the elevation data, as adding, removing and renaming files will trigger a new import. As all sub directories will be searched, it's also possible to split a data set into multiple directories if desired. ================================================ FILE: docs/linux-dev-setup.md ================================================ > [!NOTE] > Due to developer capacity constraints we cannot support newer or older compilers. > We also cannot support other versions of dependencies. > In case of doubt you can check the full release build setup used in our CI [here](https://github.com/motis-project/docker-cpp-build/blob/master/Dockerfile). > To exactly reproduce the CI build, you need to use the according preset from our [CMakePresets.json](../CMakePresets.json). Requirements: - A recent C++ compiler: Either [Clang](https://llvm.org/) 21 or GCC 13 - CMake 3.17 (or newer): [cmake.org](https://cmake.org/download/) ([Ubuntu APT Repository](https://apt.kitware.com/)) - Ninja: [ninja-build.org](https://ninja-build.org/) - Git > [!CAUTION] > Unix Makefiles are not working. Please use Ninja to build. > [!CAUTION] > Motis' dependency management `pkg` requires that the project is cloned via SSH using an SSH key without a passphrase. > See: > - [GitHub Docs: Generate new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) > - [GitHub Docs: Add a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) > [!NOTE] > Using a different compiler version is not officially supported but might work nevertheless when passing > `--compile-no-warning-as-error` to CMake. ## Build with GCC ```sh git clone git@github.com:motis-project/motis.git cd motis mkdir build && cd build CXX=g++-13 CC=gcc-13 cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -GNinja .. ninja ``` ## Build with Clang ```sh git clone git@github.com:motis-project/motis.git cd motis mkdir build && cd build CXX=clang++-21 CC=clang-21 CXXFLAGS=-stdlib=libc++ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -GNinja .. ninja ``` ================================================ FILE: docs/macos-dev-setup.md ================================================ Requirements: - macOS 10.15 or newer - Command Line Tools for Xcode or Xcode: `xcode-select --install` or [manual download](https://developer.apple.com/downloads) - [CMake](https://cmake.org/download/) 3.17 (or newer) - [Ninja](https://ninja-build.org/) - Git (Git, Ninja, and CMake can be installed via HomeBrew) > [!CAUTION] > Unix Makefiles are not working. Please use Ninja to build. > [!CAUTION] > Motis' dependency management `pkg` requires that the project is cloned via SSH using an SSH key without a passphrase. > See: > - [GitHub Docs: Generate new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) > - [GitHub Docs: Add a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) To build `motis`: ```sh git clone git@github.com:motis-project/motis.git cd motis mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -GNinja .. ninja ``` ================================================ FILE: docs/python-client.md ================================================ # Python client for MOTIS ## Install dependencies ```sh pip install openapi-python-client ``` ## Generate Python code from OpenAPI specifications ```sh openapi-python-client generate --path openapi.yaml --output-path motis_api_client --meta none ``` ## Use code (example) ```python from motis_api_client import Client from motis_api_client.api.routing import one_to_all with Client(base_url='http://localhost:8080') as client: res = one_to_all.sync(one='52.520806, 13.409420', max_travel_time=30, client=client) res ``` ================================================ FILE: docs/scripting.md ================================================ # User Scripts MOTIS can post-process GTFS static timetable data using [Lua](https://www.lua.org/) scripts. The main purpose is to fix data in case the MOTIS user is not the owner of the data nd the data owner cannot or does not want to fix the data. In some cases, the scripts can be used to homogenize data across different datasets. Currently, post-processing is available for the following entities: If no script is defined or a processing function is not given for a type, the default behaviour will be applied. ## Configuration Scripts are an optional key for each dataset in the timetable. An empty string or not setting the property indicates no processing. Any non-empty string will be interpreted as file path to a `.lua` file. The file has to exist. Example configuration with script property set: ``` timetable: datasets: nl: path: nl_ovapi.gtfs.zip rt: - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb script: my-script.lua ``` ## Types ### Translation List Some string fields are translated. Their default getter (e.g. `get_name`) now returns the default string, while the accompanying `get_*_translations` functions expose the full translation list. Lists can be accessed with [sol2 container operations](https://sol2.readthedocs.io/en/latest/containers.html). Each entry in that list is of type `translation` and provides: - `get_language` - `set_language` - `get_text` - `set_text` Example snippet of how to read and write translations: ```lua function process_route(route) route:set_short_name({ translation.new('en', 'EN_SHORT_NAME'), translation.new('de', 'DE_SHORT_NAME'), translation.new('fr', 'FR_SHORT_NAME') }) route:get_short_name_translations():add(translation.new('hu', 'HU_SHORT_NAME')) print(route:get_short_name_translations():get(1):get_text()) print(route:get_short_name_translations():get(1):get_language()) end ``` ### Location (stops, platforms, tracks) processing via `function process_location()` - `get_id` - `get_name` - `get_name_translations` - `set_name` - `get_platform_code` - `get_platform_code_translations` - `set_platform_code` - `get_description` - `get_description_translations` - `set_description` - `get_pos` - `set_pos` - `get_timezone` - `set_timezone` - `get_transfer_time` - `set_transfer_time` ### Agency (as defined in GTFS `agencies.txt`) processing via `function process_agency(agency)` - `get_id` - `get_name` - `get_name_translations` - `set_name` - `get_url` - `get_url_translations` - `set_url` - `get_timezone` - `set_timezone` ### Routes (as defined in GTFS `routes.txt`) processing via `function process_route(location)` - `get_id` - `get_short_name` - `get_short_name_translations` - `set_short_name` - `get_long_name` - `get_long_name_translations` - `set_long_name` - `get_route_type` - `set_route_type` - `get_color` - `set_color` - `get_clasz` (deprecated, use `get_route_type`) - `set_clasz` (deprecated, use `set_route_type`) - `get_text_color` - `set_text_color` - `get_agency` The following constants can be used for `set_route_type`: - `GTFS_TRAM` - `GTFS_SUBWAY` - `GTFS_RAIL` - `GTFS_BUS` - `GTFS_FERRY` - `GTFS_CABLE_TRAM` - `GTFS_AERIAL_LIFT` - `GTFS_FUNICULAR` - `GTFS_TROLLEYBUS` - `GTFS_MONORAIL` - `RAILWAY_SERVICE` - `HIGH_SPEED_RAIL_SERVICE` - `LONG_DISTANCE_TRAINS_SERVICE` - `INTER_REGIONAL_RAIL_SERVICE` - `CAR_TRANSPORT_RAIL_SERVICE` - `SLEEPER_RAIL_SERVICE` - `REGIONAL_RAIL_SERVICE` - `TOURIST_RAILWAY_SERVICE` - `RAIL_SHUTTLE_WITHIN_COMPLEX_SERVICE` - `SUBURBAN_RAILWAY_SERVICE` - `REPLACEMENT_RAIL_SERVICE` - `SPECIAL_RAIL_SERVICE` - `LORRY_TRANSPORT_RAIL_SERVICE` - `ALL_RAILS_SERVICE` - `CROSS_COUNTRY_RAIL_SERVICE` - `VEHICLE_TRANSPORT_RAIL_SERVICE` - `RACK_AND_PINION_RAILWAY_SERVICE` - `ADDITIONAL_RAIL_SERVICE` - `COACH_SERVICE` - `INTERNATIONAL_COACH_SERVICE` - `NATIONAL_COACH_SERVICE` - `SHUTTLE_COACH_SERVICE` - `REGIONAL_COACH_SERVICE` - `SPECIAL_COACH_SERVICE` - `SIGHTSEEING_COACH_SERVICE` - `TOURIST_COACH_SERVICE` - `COMMUTER_COACH_SERVICE` - `ALL_COACHS_SERVICE` - `URBAN_RAILWAY_SERVICE` - `METRO_SERVICE` - `UNDERGROUND_SERVICE` - `URBAN_RAILWAY_2_SERVICE` - `ALL_URBAN_RAILWAYS_SERVICE` - `MONORAIL_SERVICE` - `BUS_SERVICE` - `REGIONAL_BUS_SERVICE` - `EXPRESS_BUS_SERVICE` - `STOPPING_BUS_SERVICE` - `LOCAL_BUS_SERVICE` - `NIGHT_BUS_SERVICE` - `POST_BUS_SERVICE` - `SPECIAL_NEEDS_BUS_SERVICE` - `MOBILITY_BUS_SERVICE` - `MOBILITY_BUS_FOR_REGISTERED_DISABLED_SERVICE` - `SIGHTSEEING_BUS_SERVICE` - `SHUTTLE_BUS_SERVICE` - `SCHOOL_BUS_SERVICE` - `SCHOOL_AND_PUBLIC_BUS_SERVICE` - `RAIL_REPLACEMENT_BUS_SERVICE` - `DEMAND_AND_RESPONSE_BUS_SERVICE` - `ALL_BUSS_SERVICE` - `TROLLEYBUS_SERVICE` - `TRAM_SERVICE` - `CITY_TRAM_SERVICE` - `LOCAL_TRAM_SERVICE` - `REGIONAL_TRAM_SERVICE` - `SIGHTSEEING_TRAM_SERVICE` - `SHUTTLE_TRAM_SERVICE` - `ALL_TRAMS_SERVICE` - `WATER_TRANSPORT_SERVICE` - `AIR_SERVICE` - `FERRY_SERVICE` - `AERIAL_LIFT_SERVICE` - `TELECABIN_SERVICE` - `CABLE_CAR_SERVICE` - `ELEVATOR_SERVICE` - `CHAIR_LIFT_SERVICE` - `DRAG_LIFT_SERVICE` - `SMALL_TELECABIN_SERVICE` - `ALL_TELECABINS_SERVICE` - `FUNICULAR_SERVICE` - `TAXI_SERVICE` - `COMMUNAL_TAXI_SERVICE` - `WATER_TAXI_SERVICE` - `RAIL_TAXI_SERVICE` - `BIKE_TAXI_SERVICE` - `LICENSED_TAXI_SERVICE` - `PRIVATE_HIRE_VEHICLE_SERVICE` - `ALL_TAXIS_SERVICE` - `MISCELLANEOUS_SERVICE` - `HORSE_DRAWN_CARRIAGE_SERVICE` The color is currently set as unsigned 32bit integer. In future versions, we might change this to a hex string like `#FF0000`. ### Trips (as defined in GTFS `trips.txt`) processing via `function process_trip(trip)` - `get_id` - `get_headsign` - `get_headsign_translations` - `set_headsign` - `get_short_name` - `get_short_name_translations` - `set_short_name` - `get_display_name` - `get_display_name_translations` - `set_display_name` - `get_route` ### Geo Location This type is used for stop coordinates in `process_location()` for `location:get_pos()` and `location:set_pos`. - `get_lat` - `get_lng` - `set_lat` - `set_lng` ## Filtering Each processing function can return a boolean which will be interpreted as - `true`: keep this entity - `false`: don't keep this entity If nothing is returned from a process function (e.g. no return statement at all), no filtering will be applied (i.e. the default is `keep=true`). Filtering has the following effects: - In case an agency is removed, all its routes and trips will be removed as well - In case a route is removed, all its trips will be removed as well - If locations are filtered, the locations will not be removed from trips and transfers referencing those stops ## Out of Scope Scripting is currently aiming at cosmetic changes to existing entities to improve the user experience, not the creation of new entities. The creation of new entities currently has to be done outside of MOTIS in a separate preprocessing step. Currently, it is also not supported to mess with primary/foreign keys (IDs such as `trip_id`, `stop_id`, `route_ìd`). ## Example This example illustrates the usage of scripting capabilities in MOTIS. Beware that it does not make sense at all and its sole purpose is to demonstrate syntax and usage of available functionality. ```lua function process_location(stop) local name = stop:get_name() if string.sub(name, -7) == ' Berlin' then stop:set_name(string.sub(name, 1, -8)) end local pos = stop:get_pos() pos:set_lat(stop:get_pos():get_lat() + 2.0) pos:set_lng(stop:get_pos():get_lng() - 2.0) stop:set_pos(pos) stop:set_description(stop:get_description() .. ' ' .. stop:get_id() .. ' YEAH') stop:set_timezone('Europe/Berlin') stop:set_transfer_time(stop:get_transfer_time() + 98) stop:set_platform_code(stop:get_platform_code() .. 'A') return true end function process_route(route) if route:get_id() == 'R_RE4' then return false end if route:get_route_type() == 3 then route:set_clasz(7) route:set_route_type(101) elseif route:get_route_type() == 1 then route:set_clasz(8) route:set_route_type(400) end if route:get_agency():get_name() == 'Deutsche Bahn' and route:get_route_type() == 101 then route:set_short_name('RE ' .. route:get_short_name()) end return true end function process_agency(agency) if agency:get_id() == 'TT' then return false end if agency:get_name() == 'Deutsche Bahn' and agency:get_id() == 'DB' then agency:set_url(agency:get_timezone()) agency:set_timezone('Europe/Berlin') agency:set_name('SNCF') return true end return false end function process_trip(trip) if trip:get_route():get_route_type() == 101 then -- Prepend category and eliminate leading zeros (e.g. '00123' -> 'ICE 123') trip:set_short_name('ICE ' .. string.format("%d", tonumber(trip:get_short_name()))) trip:set_display_name(trip:get_short_name()) end return trip:get_id() == 'T_RE1' end ``` ## Future Work There are more attributes that could be made readable/writable such as `bikes_allowed`, `cars_allowed`. Also trip stop times and their attributes such as stop sequence numbers could be made available to scripting. Another topic not addressed yet is API versioning for the lua functions. At the moment, this feature is considered experimental which means that breaking changes might occur without prior notice. ================================================ FILE: docs/setup.md ================================================ # Advanced Configuration This is an example of how to use multiple GTFS-static datasets with multiple real-time feeds, as well as GBFS feeds. You can also see how to set additional headers like `Authorization` to enable the usage of API keys. ```yaml server: port: 8080 web_folder: ui osm: netherlands-latest.osm.pbf timetable: datasets: nl: path: nl_ovapi.gtfs.zip rt: - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb ch: path: ch_opentransportdataswiss.gtfs.zip rt: - url: https://api.opentransportdata.swiss/gtfsrt2020 headers: Authorization: MY_API_KEY protocol: gtfsrt gbfs: feeds: montreal: url: https://gbfs.velobixi.com/gbfs/gbfs.json # Example feed for header usage example-feed: url: https://example.org/gbfs headers: authorization: MY_OTHER_API_KEY other-header: other-value tiles: profile: tiles-profiles/full.lua street_routing: elevation_data_dir: srtm/ geocoding: true osr_footpath: true ``` This expands to the following configuration: ```yaml server: host: 0.0.0.0 # host (default = 0.0.0.0) port: 8080 # port (default = 8080) web_folder: ui # folder with static files to serve n_threads: 24 # default (if not set): number of hardware threads data_attribution_link: https://creativecommons.org/licenses/by/4.0/ # link to data sources or license exposed in HTTP headers and UI osm: netherlands-latest.osm.pbf # required by tiles, street routing, geocoding and reverse-geocoding tiles: # tiles won't be available if this key is missing profile: tiles-profiles/full.lua # currently `background.lua` (less details) and `full.lua` (more details) are available db_size: 1099511627776 # default size for the tiles database (influences VIRT memory usage) flush_threshold: 10000000 # usually don't change this (less = reduced memory usage during tiles import) timetable: # if not set, no timetable will be loaded first_day: TODAY # first day of timetable to load, format: "YYYY-MM-DD" (special value "TODAY") num_days: 365 # number of days to load, default is 365 days railviz: true # enable viewing vehicles in real-time on the map, requires some extra lookup data structures with_shapes: true # extract and serve shapes (if disabled, direct lines are used) adjust_footpaths: true # if footpaths are too fast, they are adjusted if set to true merge_dupes_intra_src: false # duplicates within the same datasets will be merged merge_dupes_inter_src: false # duplicates withing different datasets will be merged link_stop_distance: 100 # stops will be linked by footpaths if they're less than X meters (default=100m) apart update_interval: 60 # real-time updates are polled every `update_interval` seconds http_timeout: 30 # maximum time in seconds the real-time feed download may take incremental_rt_update: false # false = real-time updates are applied to a clean slate, true = no data will be dropped max_footpath_length: 15 # maximum footpath length when transitively connecting stops or for routing footpaths if `osr_footpath` is set to true max_matching_distance: 25.0 # maximum distance from geolocation to next OSM ways that will be found preprocess_max_matching_distance: 250.0 # max. distance for preprocessing matches from nigiri locations (stops) to OSM ways to speed up querying (set to 0 (default) to disable) datasets: # map of tag -> dataset ch: # the tag will be used as prefix for stop IDs and trip IDs with `_` as divider, so `_` cannot be part of the dataset tag path: ch_opentransportdataswiss.gtfs.zip default_bikes_allowed: false rt: - url: https://api.opentransportdata.swiss/gtfsrt2020 headers: Authorization: MY_API_KEY protocol: gtfsrt # specify the real time protocol (default: gtfsrt) nl: path: nl_ovapi.gtfs.zip default_bikes_allowed: false rt: - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb extend_calendar: true # expand the weekly service pattern beyond the end of `feed_info.txt::feed_end_date` if `feed_end_date` matches `calendar.txt::end_date` gbfs: feeds: montreal: url: https://gbfs.velobixi.com/gbfs/gbfs.json example-feed: url: https://example.org/gbfs headers: authorization: MY_OTHER_API_KEY other-header: other-value street_routing: # enable street routing (default = false; Using boolean values true/false is supported for backward compatibility) elevation_data_dir: srtm/ # folder which contains elevation data, e.g. SRTMGL1 data tiles in HGT format limits: stoptimes_max_results: 256 # maximum number of stoptimes results that can be requested plan_max_results: 256 # maximum number of plan results that can be requested via numItineraries parameter plan_max_search_window_minutes: 5760 # maximum (minutes) for searchWindow parameter (seconds), highest possible value: 21600 (15 days) onetomany_max_many: 128 # maximum accepted number of many locations for one-to-many requests onetoall_max_results: 65535 # maximum number of one-to-all results that can be requested onetoall_max_travel_minutes: 90 # maximum travel duration for one-to-all query that can be requested routing_max_timeout_seconds: 90 # maximum duration a routing query may take gtfsrt_expose_max_trip_updates: 100 # how many trip updates are allowed to be exposed via the gtfsrt endpoint street_routing_max_prepost_transit_seconds: 3600 # limit for maxPre/PostTransitTime API params, see below street_routing_max_direct_seconds: 21600 # limit for maxDirectTime API param, high values can lead to long-running, RAM-hungry queries logging: log_level: debug # log-level (default = debug; Supported log-levels: error, info, debug) osr_footpath: true # enable routing footpaths instead of using transfers from timetable datasets geocoding: true # enable geocoding for place/stop name autocompletion reverse_geocoding: false # enable reverse geocoding for mapping a geo coordinate to nearby places/addresses ``` # Scenario with Elevators This is an example configuration for Germany which enables the real-time update of elevators from Deutsche Bahn's FaSta (Facility Status) JSON API. You need to register and obtain an API key. ```yml server: web_folder: ui tiles: profile: tiles-profiles/full.lua geocoding: true street_routing: true # Alternative notion the enable street routing osr_footpath: true elevators: # init: fasta.json # Can be used for debugging, remove `url` key in this case url: https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/facilities headers: DB-Client-ID: b5d28136ffedb73474cc7c97536554df! DB-Api-Key: ef27b9ad8149cddb6b5e8ebb559ce245! osm: germany-latest.osm.pbf timetable: extend_missing_footpaths: true use_osm_stop_coordinates: true datasets: de: path: 20250331_fahrplaene_gesamtdeutschland_gtfs.zip rt: - url: https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf ``` # GBFS Configuration This examples shows how to configure multiple GBFS feeds. A GBFS feed might describe a single system or area, `callabike` in this example, or a set of feeds, that are combined to a manifest, like `mobidata-bw` here. For readability, optional headers are not included. ```yaml gbfs: feeds: # GBFS feed: callabike: url: https://api.mobidata-bw.de/sharing/gbfs/callabike/gbfs # GBFS manifest / Lamassu feed: mobidata-bw: url: https://api.mobidata-bw.de/sharing/gbfs/v3/manifest.json update_interval: 300 http_timeout: 10 ``` ## Provider Groups + Colors GBFS providers (feeds) can be grouped into "provider groups". For example, a provider may operate in multiple locations and provide a feed per location. To groups these different feeds into a single provider group, specify the same group name for each feed in the configuration. Feeds that don't have an explicit group setting in the configuration, their group name is derived from the system name. Group names may not contain commas. The API supports both provider groups and individual providers. Provider colors are loaded from the feed (`brand_assets.color`) if available, but can also be set in the configuration to override the values contained in the feed or to set colors for feeds that don't include color information. Colors can be set for groups (applies to all providers belonging to the group) or individual providers (overrides group color for that feed). ```yaml gbfs: feeds: de-CallaBike: url: https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/gbfs color: "#db0016" de-VRNnextbike: url: https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_vn/gbfs.json group: nextbike # uses the group color defined below de-NextbikeFrankfurt: url: https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_ff/gbfs.json group: nextbike de-KVV.nextbike: url: https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_fg/gbfs.json group: nextbike color: "#c30937" # override color for this particular feed groups: nextbike: # name: nextbike # Optional: Override the name (otherwise the group id, here "nextbike", is used) color: "#0046d6" ``` For aggregated feeds (manifest.json or Lamassu), groups and colors can either be assigned to all providers listed in the aggregated feed or individually by using the system_id: ```yaml gbfs: feeds: aggregated-single-group: url: https://example.com/one-provider-group/manifest.json group: Example color: "#db0016" # or assign a color to the group aggregated-multiple-groups: url: https://example.com/multiple-provider-groups/manifest.json group: source-nextbike-westbike: nextbike # "source-nextbike-westbike" is the system_id source-voi-muenster: VOI source-voi-duisburg-oberhausen: VOI # colors can be specified for individual feeds using the same syntax, # but in this example they are defined for the groups below #color: # "source-nextbike-westbike": "#0046d6" # "source-voi-muenster": "#f26961" groups: nextbike: color: "#0046d6" VOI: color: "#f26961" ``` ## HTTP Headers + OAuth If a feed requires specific HTTP headers, they can be defined like this: ```yaml gbfs: feeds: example: url: https://example.com/gbfs headers: authorization: MY_OTHER_API_KEY other-header: other-value ``` OAuth with client credentials and bearer token types is also supported: ```yaml gbfs: feeds: example: url: https://example.com/gbfs oauth: token_url: https://example.com/openid-connect/token client_id: gbfs client_secret: example ``` ## Default Restrictions A GBFS feed can define geofencing zones and rules, that apply to areas within these zones. For restrictions on areas not included in these geofencing zones, a feed may contain global rules. If these are missing, it's possible to define `default_restrictions`, that apply to either a single feed or a manifest. The following example shows possible configurations: ```yaml gbfs: feeds: # GBFS feed: #callabike: # url: https://api.mobidata-bw.de/sharing/gbfs/callabike/gbfs # GBFS manifest / Lamassu feed: mobidata-bw: url: https://api.mobidata-bw.de/sharing/gbfs/v3/manifest.json default_restrictions: mobidata-bw:callabike: # "callabike" feed contained in the "mobidata-bw" manifest # these restrictions apply outside of the defined geofencing zones if the feed doesn't contain global rules ride_start_allowed: true ride_end_allowed: true ride_through_allowed: true #station_parking: false #return_constraint: roundtrip_station #mobidata-bw: # default restrictions for all feeds contained in the "mobidata-bw" manifest #callabike: # default restrictions for standalone GBFS feed "callabike" (when not using the mobidata-bw example) update_interval: 300 http_timeout: 10 ``` # Real time protocols MOTIS supports multiple protocols for real time feeds. This section shows a list of the protocols, including some pitfalls: | Protocol | `protocol` | Note | | ---- | ---- | ---- | | GTFS-RT | `gtfsrt` | This is the default, if `protocol` is ommitted. | | SIRI Lite (XML) | `siri` | Currently limited to SIRI Lite ET, FM and SX. Still work in progress. Use with care. | | SIRI Lite (JSON) | `siri_json` | Same as `siri`, but expects JSON server responses. See below for expected JSON structure. | | VDV AUS / VDV454 | `auser` | Requires [`auser`](https://github.com/motis-project/auser) for subscription handling | ## Supported SIRI Lite services SIRI feeds are divided into multiple feeds called services (check for instance [this](https://en.wikipedia.org/wiki/Service_Interface_for_Real_Time_Information#CEN_SIRI_Functional_Services) for a list of all services). Right now MOTIS only supports parsing the "Estimated Timetable" (ET), the "Facility Monitoring" (FM) and the "Situation Exchange" (SX) SIRI services. You can see examples of such feeds [here](https://github.com/SIRI-CEN/SIRI/tree/v2.2/examples). If you are using the `siri_json` protocol, note that MOTIS expects the following JSON structure: - **Valid** SIRI Lite JSON response: ```json { "ResponseTimestamp": "2004-12-17T09:30:46-05:00", "ProducerRef": "KUBRICK", "Status": true, "MoreData": false, "EstimatedTimetableDelivery": [ ... ] } ``` - **Invalid** SIRI Lite JSON response: ```json { "Siri": { "ServiceDelivery": { "ResponseTimestamp": "2004-12-17T09:30:46-05:00", "ProducerRef": "KUBRICK", "Status": true, "MoreData": false, "EstimatedTimetableDelivery": [ ... ] } } } ``` If, as above, the two top keys `"Siri"` and `"ServiceDelivery"` are included in the JSON response, MOTIS will fail to parse the SIRI Lite feed, throwing `[VERIFY FAIL] unable to parse time ""` errors. # Shapes To enable shapes support (polylines for trips), `timetable.with_shapes` must be set to `true`. This will load shapes that are present in the datasets (e.g. GTFS shapes.txt). It is also possible to compute shapes based on OpenStreetMap data. This requires: - `timetable.with_shapes` set to `true` - `osm` data - `street_routing` set to `true` - `timetable.route_shapes` config: ```yaml timetable: # with_shapes must be set to true to enable shapes support, otherwise no shapes will be loaded or computed with_shapes: true route_shapes: # all these options are optional # available modes: # - all: route shapes for all routes, replace existing shapes from the timetable # - missing: only compute shapes for those routes that don't have existing shapes from the timetable mode: all # routing for specific clasz types can be disabled (default = all enabled) # currently long distance street routing is slow, so in this example # we disable routing shapes for COACH # (if there are shapes for the disabled clasz types in the dataset, these will still be used) clasz: COACH: false # disable shape computation for routes with more than X stops (default = no limit) # (if there are shapes for routes with more than X stops in the dataset, these will still be used) max_stops: 100 # limit the number of threads used for shape computation (default = number of hardware threads) n_threads: 6 # enable debug API endpoint (default = false) debug_api: true # if you want to use cached shapes even if the osm file has changed since the last import, set this to true (default = false) cache_reuse_old_osm_data: false # for debugging purposes, debug information can be written to files # which can be loaded into the debug ui (see osr project) debug: path: /path/to/debug/directory all: false # debug all routes all_with_beelines: false # or only those that include beelines slow: 10000 # or only those that take >10.000ms to compute # or specific trips/routes: trips: - "trip_id_1" route_ids: - "route_id_1" route_indices: # these are internal indices (e.g. from debug UI) - 123 ``` ## Cache Routed shapes can be cached to speed up later imports when a timetable dataset is updated. If enabled, this will generate an additional cache file. This cache file and the routed shapes data are then reused during import. Note that old routes are never removed from the routed shapes data files, i.e., these files grow with every import (unless there are no new routes, in which case the size will stay the same). It is therefore recommended to monitor the size of the "routed_shapes\*" files in the data directory. They can safely be deleted before an import, which will cause all shapes that are needed for the current datasets to be routed again. The cache only applies to routed shapes, not shapes contained in the timetables. ================================================ FILE: docs/windows-dev-setup.md ================================================ In the following, we list requirements and a download link. There may be other sources (like package managers) to install these. - CMake 3.17 (or newer): [cmake.org](https://cmake.org/download/) - Git: [git-scm.com](https://git-scm.com/download/win) - Visual Studio 2022 or at least "Build Tools for Visual Studio 2022": [visualstudio.microsoft.com](https://visualstudio.microsoft.com/de/downloads/) - Ninja: [ninja-build.org](https://ninja-build.org/) > [!CAUTION] > Motis' dependency management `pkg` requires that the project is cloned via SSH using an SSH key without a passphrase. > See: > - [GitHub Docs: Generate new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) > - [GitHub Docs: Add a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) ## Build MOTIS using the command line Start menu -> `Visual Studio 2022` -> `x64 Native Tools Command Prompt for VS 2022`, then enter: ```bat git clone "git@github.com:motis-project/motis.git" cd motis mkdir build cd build cmake -GNinja -DCMAKE_BUILD_TYPE=Release .. ninja ``` ## Build MOTIS using CLion - Make sure that the architecture is set to `amd64` (Settings -> `Build, Execution, Deployment` -> `Toolchains`). - You might have to allow users to create symbolic links. Open `Local Security Policy` and go to `User Rights Assignment`. - You might have to enable `Developer Mode` under `Advanced System Settings`. ================================================ FILE: exe/batch.cc ================================================ #include #include #include "conf/configuration.h" #include "utl/init_from.h" #include "utl/parallel_for.h" #include "utl/parser/cstr.h" #include "motis/config.h" #include "motis/data.h" #include "motis/motis_instance.h" #include "./flags.h" namespace fs = std::filesystem; namespace po = boost::program_options; namespace json = boost::json; struct thousands_sep : std::numpunct { char_type do_thousands_sep() const override { return ','; } string_type do_grouping() const override { return "\3"; } }; struct stats { struct entry { bool operator<(entry const& o) const { return value_ < o.value_; } std::uint64_t msg_id_, value_; }; stats() = default; stats(std::string name, std::uint64_t count_so_far) : name_{std::move(name)}, values_{count_so_far} {} void add(uint64_t msg_id, std::uint64_t value) { values_.emplace_back(entry{msg_id, value}); sum_ += value; } std::string name_; std::vector values_; std::uint64_t sum_{}; }; struct category { category() = default; explicit category(std::string name) : name_(std::move(name)) {} std::string name_; std::map stats_; }; stats::entry quantile(std::vector const& sorted_values, double q) { if (q == 1.0) { return sorted_values.back(); } else { return sorted_values[std::min( static_cast(std::round(q * (sorted_values.size() - 1))), sorted_values.size() - 1)]; } } void print_category(category& cat, std::uint64_t count, bool const compact, int const top) { std::cout << "\n" << cat.name_ << "\n" << std::string(cat.name_.size(), '=') << "\n" << std::endl; for (auto& s : cat.stats_) { auto& stat = s.second; if (stat.values_.empty()) { continue; } utl::sort(stat.values_); auto const avg = (stat.sum_ / static_cast(count)); if (compact) { std::cout << std::left << std::setw(30) << stat.name_ << " avg: " << std::setw(27) << std::setprecision(4) << std::fixed << avg << " Q(99): " << std::setw(25) << quantile(stat.values_, 0.99).value_ << " Q(90): " << std::setw(22) << quantile(stat.values_, 0.9).value_ << " Q(80): " << std::setw(22) << quantile(stat.values_, 0.8).value_ << " Q(50): " << std::setw(22) << quantile(stat.values_, 0.5).value_; auto const from = static_cast( std::max(static_cast(0L), static_cast(stat.values_.size()) - static_cast(top))); for (auto i = from; i != stat.values_.size(); ++i) { auto const i_rev = stat.values_.size() - (i - from) - 1; std::cout << "(v=" << stat.values_[i_rev].value_ << ", i=" << stat.values_[i_rev].msg_id_ << ")"; if (i != stat.values_.size() - 1) { std::cout << ", "; } } std::cout << std::endl; } else { std::cout << stat.name_ << "\n average: " << std::right << std::setw(15) << std::setprecision(2) << std::fixed << avg << "\n max: " << std::right << std::setw(12) << std::max_element(begin(stat.values_), end(stat.values_))->value_ << "\n 99 quantile: " << std::right << std::setw(12) << quantile(stat.values_, 0.99).value_ << "\n 90 quantile: " << std::right << std::setw(12) << quantile(stat.values_, 0.9).value_ << "\n 80 quantile: " << std::right << std::setw(12) << quantile(stat.values_, 0.8).value_ << "\n 50 quantile: " << std::right << std::setw(12) << quantile(stat.values_, 0.5).value_ << "\n min: " << std::right << std::setw(12) << std::min_element(begin(stat.values_), end(stat.values_))->value_ << "\n" << std::endl; } } } namespace motis { int batch(int ac, char** av) { auto data_path = fs::path{"data"}; auto queries_path = fs::path{"queries.txt"}; auto responses_path = fs::path{"responses.txt"}; auto mt = true; auto desc = po::options_description{"Options"}; desc.add_options() // ("help", "Prints this help message") // ("multithreading,mt", po::value(&mt)->default_value(mt)) // ("queries,q", po::value(&queries_path)->default_value(queries_path), "queries file") // ("responses,r", po::value(&responses_path)->default_value(responses_path), "response file"); add_data_path_opt(desc, data_path); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return 0; } auto queries = std::vector{}; auto f = cista::mmap{queries_path.generic_string().c_str(), cista::mmap::protection::READ}; utl::for_each_line(utl::cstr{f.view()}, [&](utl::cstr s) { queries.push_back(s.view()); }); auto const c = config::read(data_path / "config.yml"); utl::verify(c.timetable_.has_value(), "timetable required"); auto d = data{data_path, c}; utl::verify(d.tt_, "timetable required"); auto response_time = stats{"response_time", 0U}; struct state {}; auto out = std::ofstream{responses_path}; auto m = motis_instance{net::default_exec{}, d, c, ""}; auto const compute_response = [&](state&, std::size_t const id) { UTL_START_TIMING(request); auto response = std::string{}; try { m.qr_( {boost::beast::http::verb::get, boost::beast::string_view{queries.at(id)}, 11}, [&](net::web_server::http_res_t const& res) { std::visit( [&](auto&& r) { using ResponseType = std::decay_t; if constexpr (std::is_same_v) { response = r.body(); if (response.empty()) { std::cout << "empty response for " << id << ": " << queries.at(id) << " [status=" << r.result() << "]\n"; } } else { throw utl::fail("not a valid response type: {}", cista::type_str()); } }, res); }, false); } catch (std::exception const& e) { std::cerr << "ERROR IN QUERY " << id << ": " << e.what() << "\n"; } return std::pair{UTL_GET_TIMING_MS(request), std::move(response)}; }; auto const pt = utl::activate_progress_tracker("batch"); pt->in_high(queries.size()); if (mt) { utl::parallel_ordered_collect_threadlocal( queries.size(), compute_response, [&](std::size_t const id, std::pair const& s) { response_time.add(id, s.first); out << s.second << "\n"; }, pt->update_fn()); } else { auto s = state{}; for (auto i = 0U; i != queries.size(); ++i) { compute_response(s, i); pt->increment(); } } auto cat = category{}; cat.name_ = "response_time"; cat.stats_.emplace("response_time", std::move(response_time)); std::cout.imbue(std::locale(std::locale::classic(), new thousands_sep)); print_category(cat, queries.size(), false, 10U); return 0U; } } // namespace motis ================================================ FILE: exe/compare.cc ================================================ #include #include #include #include #include #include "conf/configuration.h" #include "boost/json/parse.hpp" #include "boost/json/serialize.hpp" #include "boost/json/value_from.hpp" #include "boost/json/value_to.hpp" #include "fmt/std.h" #include "utl/enumerate.h" #include "utl/file_utils.h" #include "utl/get_or_create.h" #include "utl/helpers/algorithm.h" #include "utl/overloaded.h" #include "utl/sorted_diff.h" #include "utl/to_vec.h" #include "utl/verify.h" #include "motis-api/motis-api.h" #include "motis/types.h" #include "./flags.h" namespace fs = std::filesystem; namespace po = boost::program_options; namespace json = boost::json; namespace motis { int compare(int ac, char** av) { auto subset_check = false; auto queries_path = fs::path{"queries.txt"}; auto responses_paths = std::vector{}; auto fails_path = fs::path{"fail"}; auto desc = po::options_description{"Options"}; desc.add_options() // ("help", "Prints this help message") // ("queries,q", po::value(&queries_path)->default_value(queries_path), "queries file") // ("subset_check", po::value(&subset_check)->default_value(subset_check), "only check subset ([1...N] <= [0])") // ("responses,r", po::value(&responses_paths) ->multitoken() ->default_value(responses_paths), "response files"); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return 0; } auto const write_fails = fs::is_directory(fails_path); if (!write_fails) { fmt::println("{} is not a directory, not writing fails", fails_path); } struct info { unsigned id_; std::optional params_{}; std::vector> responses_{}; }; auto const params = [](api::Itinerary const& x) { return std::tie(x.startTime_, x.endTime_, x.transfers_); }; auto const equal = [&](std::vector const& a, std::vector const& b) { if (subset_check) { return utl::all_of(a, [&](api::Itinerary const& x) { return utl::any_of( b, [&](api::Itinerary const& y) { return params(x) == params(y); }); }); } else { return std::ranges::equal(a | std::views::transform(params), b | std::views::transform(params)); } }; auto const print_params = [](api::Itinerary const& x) { std::cout << x.startTime_ << ", " << x.endTime_ << ", transfers=" << std::setw(2) << std::left << x.transfers_; }; auto const print_none = []() { std::cout << "\t\t\t\t\t\t"; }; auto n_equal = 0U; auto const print_differences = [&](info const& x) { auto const is_incomplete = utl::any_of(x.responses_, [](auto&& x) { return !x.has_value(); }); auto const ref = x.responses_[0].value_or(api::plan_response{}).itineraries_; auto mismatch = false; for (auto i = 1U; i < x.responses_.size(); ++i) { mismatch |= !x.responses_[i].has_value(); auto const uut = x.responses_[i].value_or(api::plan_response{}).itineraries_; if (equal(ref, uut)) { ++n_equal; continue; } mismatch = true; std::cout << "QUERY=" << x.id_ << " [" << x.params_->to_url("/api/v1/plan") << "]"; if (is_incomplete) { std::cout << " [INCOMPLETE!!]"; } std::cout << "\n"; utl::sorted_diff( ref, uut, [&](api::Itinerary const& a, api::Itinerary const& b) { return params(a) < params(b); }, [&](api::Itinerary const&, api::Itinerary const&) { return false; // always call for equal }, utl::overloaded{ [&](utl::op op, api::Itinerary const& j) { if (op == utl::op::kAdd) { print_none(); std::cout << "\t\t\t\t"; print_params(j); std::cout << "\n"; } else { print_params(j); std::cout << "\t\t\t\t"; print_none(); std::cout << "\n"; } }, [&](api::Itinerary const& a, api::Itinerary const& b) { print_params(a); std::cout << "\t\t\t"; print_params(b); std::cout << "\n"; }}); std::cout << "\n\n"; } if (mismatch && write_fails) { std::ofstream{fails_path / fmt::format("{}_q.txt", x.id_)} << x.params_->to_url("/api/v1/plan") << "\n"; for (auto i = 0U; i < x.responses_.size(); ++i) { if (!x.responses_[i].has_value()) { continue; } std::ofstream{fails_path / fmt::format("{}_{}.json", x.id_, i)} << json::serialize(json::value_from(x.responses_[i].value())) << "\n"; } } }; auto query_file = utl::open_file(queries_path); auto responses_files = utl::to_vec(responses_paths, [&](auto&& p) { return utl::open_file(p); }); auto n_consumed = 0U; auto query_id = 0U; while (true) { auto nfo = info{.id_ = ++query_id, .responses_ = std::vector>{ responses_files.size()}}; if (auto const q = utl::read_line(query_file); q.has_value()) { nfo.params_ = api::plan_params{boost::urls::url{*q}.params()}; } else { break; } for (auto const [i, res_file] : utl::enumerate(responses_files)) { if (auto const r = utl::read_line(res_file); r.has_value()) { try { auto val = boost::json::parse(*r); if (val.is_object() && val.as_object().contains("requestParameters")) { auto res = json::value_to(val); utl::sort(res.itineraries_, [&](auto&& a, auto&& b) { return params(a) < params(b); }); nfo.responses_[i] = std::move(res); } } catch (...) { } } else { break; } } print_differences(nfo); ++n_consumed; } std::cout << "consumed: " << n_consumed << "\n"; std::cout << " equal: " << n_equal << "\n"; return n_consumed == n_equal ? 0 : 1; } } // namespace motis ================================================ FILE: exe/extract.cc ================================================ #include #include #include "boost/json.hpp" #include "boost/program_options.hpp" #include "fmt/ranges.h" #include "fmt/std.h" #include "utl/parser/buf_reader.h" #include "utl/parser/csv_range.h" #include "utl/parser/split.h" #include "utl/pipes.h" #include "utl/progress_tracker.h" #include "utl/verify.h" #include "nigiri/loader/dir.h" #include "nigiri/common/interval.h" #include "motis-api/motis-api.h" #include "motis/tag_lookup.h" #include "motis/types.h" #include "flags.h" namespace po = boost::program_options; namespace fs = std::filesystem; namespace n = nigiri; namespace motis { void copy_stop_times(hash_set const& trip_ids, hash_set const& filter_stop_ids, std::string_view file_content, hash_set& stop_ids, std::ostream& out) { struct csv_stop_time { utl::csv_col trip_id_; utl::csv_col stop_id_; }; auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (trip_ids.contains(row.trip_id_->view()) && (filter_stop_ids.empty() || filter_stop_ids.contains(row.stop_id_->view()))) { stop_ids.insert(row.stop_id_->view()); out << line.view() << "\n"; ++n_lines; } } fmt::println(" stop_times.txt: lines written: {}", n_lines); } void copy_stops(hash_set& stop_ids, std::string_view file_content, std::ostream& out, bool const filter_stops) { struct csv_stop { utl::csv_col stop_id_; utl::csv_col parent_station_; }; { // First pass: collect parents. auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (!row.parent_station_->empty() && stop_ids.contains(row.stop_id_->view())) { stop_ids.emplace(row.parent_station_->view()); } } } { // Second pass: copy contents. auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { if (filter_stops) { auto const row = utl::read_row(header_permutation, line); if (stop_ids.contains(row.stop_id_->view())) { out << line.view() << "\n"; ++n_lines; } } else { out << line.view() << "\n"; } } fmt::println(" stops.txt: lines written: {}", n_lines); } } void copy_trips(hash_set const& trip_ids, std::string_view file_content, hash_set& route_ids, hash_set& service_ids, std::ostream& out) { struct csv_trip { utl::csv_col trip_id_; utl::csv_col route_id_; utl::csv_col service_id_; }; auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (trip_ids.contains(row.trip_id_->view())) { route_ids.insert(row.route_id_->view()); service_ids.insert(row.service_id_->view()); out << line.view() << "\n"; ++n_lines; } } fmt::println(" trips.txt: lines written: {}", n_lines); } void copy_calendar(hash_set const& service_ids, std::string_view file_content, std::ostream& out) { struct csv_service { utl::csv_col service_id_; }; auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (service_ids.contains(row.service_id_->view())) { out << line.view() << "\n"; ++n_lines; } } fmt::println(" calendar.txt / calendar_dates.txt: lines written: {}", n_lines); } void copy_routes(hash_set const& route_ids, std::string_view file_content, hash_set& agency_ids, std::ostream& out) { struct csv_service { utl::csv_col route_id_; utl::csv_col agency_id_; }; auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (route_ids.contains(row.route_id_->view())) { agency_ids.insert(row.agency_id_->view()); out << line.view() << "\n"; ++n_lines; } } fmt::println(" routes.txt: lines written: {}", n_lines); } void copy_agencies(hash_set const& agency_ids, std::string_view file_content, std::ostream& out) { struct csv_stop { utl::csv_col agency_id_; }; auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (agency_ids.contains(row.agency_id_->view())) { out << line.view() << "\n"; ++n_lines; } } fmt::println(" agencies.txt: lines written: {}", n_lines); } void copy_transfers(hash_set const& stop_ids, std::string_view file_content, std::ostream& out) { struct csv_stop { utl::csv_col from_stop_id_; utl::csv_col to_stop_id_; }; auto n_lines = 0U; auto reader = utl::make_buf_reader(file_content); auto line = reader.read_line(); auto const header_permutation = utl::read_header(line); out << line.view() << "\n"; while ((line = reader.read_line())) { auto const row = utl::read_row(header_permutation, line); if (stop_ids.contains(row.from_stop_id_->view()) && stop_ids.contains(row.to_stop_id_->view())) { out << line.view() << "\n"; ++n_lines; } } fmt::println(" transfers.txt: lines written: {}", n_lines); } int extract(int ac, char** av) { auto in = std::vector{"response.json"}; auto out = fs::path{"gtfs"}; auto reduce = false; auto filter_stops = true; auto desc = po::options_description{"Options"}; desc.add_options() // ("help", "Prints this help message") // ("reduce", po::value(&reduce)->default_value(reduce), "Only extract first and last stop of legs for stop times") // ("filter_stops", po::value(&filter_stops)->default_value(filter_stops), "Filter stops") // ("in,i", po::value(&in)->multitoken(), "PlanResponse JSON input files") // ("out,o", po::value(&out), "output directory"); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return 0; } auto important_stops = hash_set{}; auto const add_important_stop = [&](api::Place const& p) { if (!reduce || p.vertexType_ != api::VertexTypeEnum::TRANSIT) { return; } auto const tag_end = p.stopId_.value().find('_'); utl::verify(tag_end != std::string::npos, "no tag found for stop id {}", p.stopId_.value()); auto const [_, added] = important_stops.insert(p.stopId_.value().substr(tag_end + 1U)); if (added) { fmt::println("important stop {}", p.stopId_.value().substr(tag_end + 1U)); } }; auto todos = hash_map>{}; auto source = std::string{}; auto from_line = std::string{}; auto to_line = std::string{}; auto path = std::string{}; for (auto const& x : in) { auto const f = cista::mmap{x.generic_string().c_str(), cista::mmap::protection::READ}; auto const res = boost::json::value_to(boost::json::parse(f.view())); fmt::println("found {} itineraries", res.itineraries_.size()); for (auto const& i : res.itineraries_) { for (auto const& l : i.legs_) { add_important_stop(l.from_); add_important_stop(l.to_); if (!l.source_.has_value() || !l.tripId_.has_value()) { continue; } source.resize(l.source_->size()); std::reverse_copy(begin(*l.source_), end(*l.source_), begin(source)); auto const [to, from, p] = utl::split<':', utl::cstr, utl::cstr, utl::cstr>(source); from_line.resize(from.length()); std::reverse_copy(begin(from), end(from), begin(from_line)); to_line.resize(to.length()); std::reverse_copy(begin(to), end(to), begin(to_line)); path.resize(p.length()); std::reverse_copy(begin(p), end(p), begin(path)); auto const trip_id = split_trip_id(*l.tripId_); auto const [_, added] = todos[path].emplace(trip_id.trip_id_); if (added) { fmt::println("added {}:{}:{}, trip_id={}", path, from_line, to_line, trip_id.trip_id_); } } } } auto stop_ids = hash_set{}; auto route_ids = hash_set{}; auto service_ids = hash_set{}; auto agency_ids = hash_set{}; for (auto const& [stop_times_str, trip_ids] : todos) { auto const stop_times_path = fs::path{stop_times_str}; stop_ids.clear(); route_ids.clear(); service_ids.clear(); agency_ids.clear(); utl::verify(stop_times_path.filename() == "stop_times.txt", "expected filename stop_times.txt, got \"{}\"", stop_times_path); auto const dataset_dir = stop_times_path.parent_path(); utl::verify(stop_times_path.has_parent_path() && fs::exists(dataset_dir), "expected path \"{}\" to have existent parent path", stop_times_path); auto const dir = n::loader::make_dir(dataset_dir); utl::verify(dir->exists("stop_times.txt"), "no stop_times.txt file found in {}", dataset_dir); auto ec = std::error_code{}; fs::create_directories(out / dataset_dir.filename(), ec); { fmt::println("writing {}/stop_times.txt, searching for trips={}", out / dataset_dir.filename(), trip_ids); auto of = std::ofstream{out / dataset_dir.filename() / "stop_times.txt"}; fmt::println("important stops: {}", important_stops); copy_stop_times(trip_ids, important_stops, dir->get_file("stop_times.txt").data(), stop_ids, of); } { fmt::println("writing {}/stops.txt, searching for stops={}", out / dataset_dir.filename(), stop_ids); auto of = std::ofstream{out / dataset_dir.filename() / "stops.txt"}; copy_stops(stop_ids, dir->get_file("stops.txt").data(), of, filter_stops); } { fmt::println("writing {}/trips.txt", out / dataset_dir.filename()); auto of = std::ofstream{out / dataset_dir.filename() / "trips.txt"}; copy_trips(trip_ids, dir->get_file("trips.txt").data(), route_ids, service_ids, of); } { fmt::println("writing {}/routes.txt, searching for routes={}", out / dataset_dir.filename(), route_ids); auto of = std::ofstream{out / dataset_dir.filename() / "routes.txt"}; copy_routes(route_ids, dir->get_file("routes.txt").data(), agency_ids, of); } if (dir->exists("calendar.txt")) { fmt::println("writing {}/calendar.txt, searching for service_ids={}", out / dataset_dir.filename(), service_ids); auto of = std::ofstream{out / dataset_dir.filename() / "calendar.txt"}; copy_calendar(service_ids, dir->get_file("calendar.txt").data(), of); } if (dir->exists("calendar_dates.txt")) { fmt::println( "writing {}/calendar_dates.txt, searching for service_ids={}", out / dataset_dir.filename(), service_ids); auto of = std::ofstream{out / dataset_dir.filename() / "calendar_dates.txt"}; copy_calendar(service_ids, dir->get_file("calendar_dates.txt").data(), of); } if (dir->exists("agency.txt")) { fmt::println("writing {}/agency.txt, searching for agencies={}", out / dataset_dir.filename(), agency_ids); auto of = std::ofstream{out / dataset_dir.filename() / "agency.txt"}; copy_agencies(agency_ids, dir->get_file("agency.txt").data(), of); } if (dir->exists("transfers.txt")) { fmt::println("writing {}/transfers.txt", out / dataset_dir.filename()); auto of = std::ofstream{out / dataset_dir.filename() / "transfers.txt"}; copy_transfers(stop_ids, dir->get_file("transfers.txt").data(), of); } if (dir->exists("feed_info.txt")) { std::ofstream{out / dataset_dir.filename() / "feed_info.txt"} << dir->get_file("feed_info.txt").data(); } } return 0; } } // namespace motis ================================================ FILE: exe/flags.h ================================================ #pragma once #include #include #include #include "boost/program_options.hpp" namespace motis { inline void add_help_opt(boost::program_options::options_description& desc) { desc.add_options()("help,h", "print this help message"); } inline void add_data_path_opt(boost::program_options::options_description& desc, std::filesystem::path& p) { desc.add_options() // ("data,d", boost::program_options::value(&p)->default_value(p), "The data path contains all preprocessed data as well as a " "`config.yml`. " "It will be created by the `motis import` command. After the import has " "finished, `motis server` only needs the `data` folder and can run " "without the input files (such as OpenStreetMap file, GTFS datasets, " "tiles-profiles, etc.)"); } inline void add_config_path_opt( boost::program_options::options_description& desc, std::filesystem::path& p) { desc.add_options() // ("config,c", boost::program_options::value(&p)->default_value(p), "Configuration YAML file. Legacy INI files are still supported but this " "support will be dropped in the future."); } inline void add_trip_id_opt(boost::program_options::options_description& desc) { desc.add_options()( "trip-id,t", boost::program_options::value>()->composing(), "Add trip-id to analyze.\n" "If the trip-id is encoded, it will be decoded automatically.\n" "This option can be used multiple times.\n" "\n" "Will search the shape corresponding to each trip-id. " "If a shape is found, the index of the shape point, that is " "matched with each stop, will be printed.\n" "Notice that the first and last stop of a trip will always be " "matched with the first and last shape point respectively.\n" "If a shape contains less points than stops in the trip, this " "segmentation is not possible."); } inline void add_log_level_opt(boost::program_options::options_description& desc, std::string& log_lvl) { desc.add_options()("log-level", boost::program_options::value(&log_lvl), "Set the log level.\n" "Supported log levels: error, info, debug"); } inline boost::program_options::variables_map parse_opt( int ac, char** av, boost::program_options::options_description& desc) { namespace po = boost::program_options; auto vm = po::variables_map{}; po::store(po::command_line_parser(ac, av).options(desc).run(), vm); po::notify(vm); return vm; } } // namespace motis ================================================ FILE: exe/generate.cc ================================================ #include #include #include #include "conf/configuration.h" #include "boost/url/url.hpp" #include "nigiri/common/interval.h" #include "nigiri/routing/raptor/debug.h" #include "nigiri/routing/search.h" #include "nigiri/timetable.h" #include "utl/progress_tracker.h" #include "motis-api/motis-api.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/routing.h" #include "motis/odm/bounds.h" #include "motis/point_rtree.h" #include "motis/tag_lookup.h" #include "./flags.h" namespace n = nigiri; namespace fs = std::filesystem; namespace po = boost::program_options; namespace motis { constexpr auto const kMinRank = 16UL; static std::atomic_uint32_t seed{0U}; std::uint32_t rand_in(std::uint32_t const from, std::uint32_t const to) { auto a = ++seed; a = (a ^ 61U) ^ (a >> 16U); a = a + (a << 3U); a = a ^ (a >> 4U); a = a * 0x27d4eb2d; a = a ^ (a >> 15U); return from + (a % (to - from)); } template It rand_in(It const begin, It const end) { return std::next( begin, rand_in(0U, static_cast(std::distance(begin, end)))); } template Collection::value_type rand_in(Collection const& c) { using std::begin; using std::end; utl::verify(!c.empty(), "empty collection"); return *rand_in(begin(c), end(c)); } n::location_idx_t random_stop(n::timetable const& tt, std::vector const& stops) { auto s = n::location_idx_t::invalid(); do { s = rand_in(stops); } while (tt.location_routes_[s].empty()); return s; } int generate(int ac, char** av) { auto data_path = fs::path{"data"}; auto n = 100U; auto first_day = std::optional{}; auto last_day = std::optional{}; auto time_of_day = std::optional{}; auto modes = std::optional>{}; auto max_dist = 800.0; // m auto use_walk = false; auto use_bike = false; auto use_car = false; auto use_odm = false; auto lb_rank = true; auto p = api::plan_params{}; auto const parse_date = [](std::string_view const s) { std::stringstream in; in.exceptions(std::ios::badbit | std::ios::failbit); in << s; auto d = date::sys_days{}; in >> date::parse("%Y-%m-%d", d); return d; }; auto const parse_first_day = [&](std::string_view const s) { first_day = parse_date(s); }; auto const parse_last_day = [&](std::string_view const s) { last_day = parse_date(s); }; auto const parse_modes = [&](std::string_view const s) { modes = std::vector{}; if (s.contains("WALK")) { modes->emplace_back(api::ModeEnum::WALK); use_walk = true; } if (s.contains("BIKE")) { modes->emplace_back(api::ModeEnum::BIKE); use_bike = true; } if (s.contains("CAR")) { modes->emplace_back(api::ModeEnum::CAR); use_car = true; } if (s.contains("ODM")) { modes->emplace_back(api::ModeEnum::ODM); use_odm = true; } if (s.contains("RIDE_SHARING")) { modes->emplace_back(api::ModeEnum::RIDE_SHARING); use_odm = true; } }; auto const parse_time_of_day = [&](std::uint32_t const h) { time_of_day = h % 24U; }; auto desc = po::options_description{"Options"}; desc.add_options() // ("help", "Prints this help message") // ("n,n", po::value(&n)->default_value(n), "number of queries") // ("first_day", po::value()->notifier(parse_first_day), "first day of query generation, format: YYYY-MM-DD") // ("last_day", po::value()->notifier(parse_last_day), "last day of query generation, format: YYYY-MM-DD") // ("time_of_day", po::value()->notifier(parse_time_of_day), "fixes the time of day of all queries to the given number of hours " "after midnight, i.e., 0 - 23") // ("modes,m", po::value()->notifier(parse_modes), "comma-separated list of modes for first/last mile and " "direct (requires " "street routing), supported: WALK, BIKE, CAR, ODM") // ("all,a", "requires OSM nodes to be accessible by all specified modes, otherwise " "OSM nodes accessible by at least one mode are eligible, only used for " "intermodal queries") // ("max_dist", po::value(&max_dist)->default_value(max_dist), "maximum distance from a public transit stop in meters, only used for " "intermodal queries") // ("max_travel_time", po::value()->notifier( [&](auto const v) { p.maxTravelTime_ = v; }), "sets maximum travel time of the queries") // ("max_matching_distance", po::value(&p.maxMatchingDistance_) ->default_value(p.maxMatchingDistance_), "sets the maximum matching distance of the queries") // ("fastest_direct_factor", po::value(&p.fastestDirectFactor_) ->default_value(p.fastestDirectFactor_), "sets fastest direct factor of the queries") // ("lb_rank", po::value(&lb_rank)->default_value(lb_rank), "emit queries uniformly distributed over the lower bounds (lb) ranks, " "lb rank n: 2^n-th stop when sorting all stops by their lb value from " "the start (min. rank: 4, max. rank: derived from number of eligible " "stops)"); add_data_path_opt(desc, data_path); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return 0; } auto const c = config::read(data_path / "config.yml"); utl::verify(c.timetable_.has_value(), "timetable required"); utl::verify(!modes || c.use_street_routing(), "intermodal requires street routing"); auto d = data{data_path, c}; utl::verify(d.tt_, "timetable required"); first_day = first_day ? d.tt_->date_range_.clamp(*first_day) : std::chrono::time_point_cast( d.tt_->external_interval().from_); last_day = last_day ? d.tt_->date_range_.clamp( std::max(*first_day + date::days{1U}, *last_day)) : d.tt_->date_range_.clamp(*first_day + date::days{14U}); if (*first_day == *last_day) { fmt::println( "can not generate queries: date range [{}, {}] has zero length after " "clamping", *first_day, *last_day); return 1; } fmt::println("date range: [{}, {}], tt={}", *first_day, *last_day, d.tt_->external_interval()); auto const use_odm_bounds = modes && use_odm && d.odm_bounds_ != nullptr; auto node_rtree = point_rtree{}; if (modes) { if (modes->empty()) { fmt::println( "can not generate queries: provided modes option without valid " "mode"); return 1; } std::cout << "modes:"; for (auto const m : *modes) { std::cout << " " << m; } std::cout << "\n"; p.directModes_ = *modes; p.preTransitModes_ = *modes; p.postTransitModes_ = *modes; auto const mode_match = [&](auto const node) { auto const can_walk = [&](auto const x) { return utl::any_of(d.w_->r_->node_ways_[x], [&](auto const w) { return d.w_->r_->way_properties_[w].is_foot_accessible(); }); }; auto const can_bike = [&](auto const x) { return utl::any_of(d.w_->r_->node_ways_[x], [&](auto const w) { return d.w_->r_->way_properties_[w].is_bike_accessible(); }); }; auto const can_car = [&](auto const x) { return utl::any_of(d.w_->r_->node_ways_[x], [&](auto const w) { return d.w_->r_->way_properties_[w].is_car_accessible(); }); }; return vm.count("all") ? ((!use_walk || can_walk(node)) && (!use_bike || can_bike(node)) && (!(use_car || use_odm) || can_car(node))) : ((use_walk && can_walk(node)) || (use_bike && can_bike(node)) || ((use_car || use_odm) && can_car(node))); }; auto const in_bounds = [&](auto const& pos) { return !use_odm_bounds || d.odm_bounds_->contains(pos); }; for (auto i = osr::node_idx_t{0U}; i < d.w_->n_nodes(); ++i) { if (mode_match(i) && in_bounds(d.w_->get_node_pos(i))) { node_rtree.add(d.w_->get_node_pos(i), i); } } } else { fmt::println("station-to-station"); } auto stops = std::vector{}; for (auto i = 0U; i != d.tt_->n_locations(); ++i) { auto const l = n::location_idx_t{i}; if (use_odm_bounds && !d.odm_bounds_->contains(d.tt_->locations_.coordinates_[l])) { continue; } stops.emplace_back(l); } auto ss = std::optional{}; auto rs = std::optional{}; if (lb_rank) { ss = n::routing::search_state{}; rs = n::routing::raptor_state{}; fmt::println("from and to pairings by lower bounds rank"); } else { fmt::println("from and to uniformly at random"); } auto const get_place = [&](n::location_idx_t const l) -> std::optional { if (!modes) { return d.tags_->id(*d.tt_, l); } auto const nodes = node_rtree.in_radius(d.tt_->locations_.coordinates_[l], max_dist); if (nodes.empty()) { return std::nullopt; } auto const pos = d.w_->get_node_pos(rand_in(nodes)); return fmt::format("{},{}", pos.lat(), pos.lng()); }; auto const random_from_to = [&](auto const r) { auto from_place = std::optional{}; auto to_place = std::optional{}; for (auto x = 0U; x != 1000U; ++x) { auto const from_stop = random_stop(*d.tt_, stops); from_place = get_place(from_stop); if (!from_place) { continue; } if (lb_rank) { auto const s = n::routing::search< n::direction::kBackward, n::routing::raptor>{ *d.tt_, nullptr, *ss, *rs, nigiri::routing::query{ .start_time_ = d.tt_->date_range_.from_, .destination_ = {{from_stop, n::duration_t{0U}, 0}}}}; utl::sort(stops, [&](auto const& a, auto const& b) { return ss->travel_time_lower_bound_[to_idx(a)] < ss->travel_time_lower_bound_[to_idx(b)]; }); to_place = get_place(stops[r]); } else { to_place = get_place(random_stop(*d.tt_, stops)); } if (to_place) { break; } } p.fromPlace_ = *from_place; p.toPlace_ = *to_place; }; auto const random_time = [&]() { using namespace std::chrono_literals; p.time_ = *first_day + rand_in(0U, static_cast((*last_day - *first_day).count())) * date::days{1U} + (time_of_day ? *time_of_day : rand_in(6U, 18U)) * 1h; }; { auto out = std::ofstream{"queries.txt"}; auto const progress_tracker = utl::activate_progress_tracker(fmt::format("generating {} queries", n)); progress_tracker->in_high(n); auto const silencer = utl::global_progress_bars{false}; for (auto [i, r] = std::tuple{0U, kMinRank}; i != n; ++i, r = r * 2U < stops.size() ? r * 2U : kMinRank) { random_from_to(r); random_time(); out << p.to_url("/api/v1/plan") << "\n"; progress_tracker->increment(); } } return 0; } } // namespace motis ================================================ FILE: exe/main.cc ================================================ #include #include #include #include #include #include "boost/program_options.hpp" #include "boost/url/decode_view.hpp" #include "google/protobuf/stubs/common.h" #include "utl/progress_tracker.h" #include "utl/to_vec.h" #include "nigiri/rt/util.h" #include "motis/analyze_shapes.h" #include "motis/config.h" #include "motis/data.h" #include "motis/import.h" #include "motis/logging.h" #include "motis/server.h" #include "./flags.h" #if defined(USE_MIMALLOC) && defined(_WIN32) #include "mimalloc-new-delete.h" #endif #if !defined(MOTIS_VERSION) #define MOTIS_VERSION "unknown" #endif namespace po = boost::program_options; using namespace std::string_view_literals; namespace fs = std::filesystem; namespace motis { int generate(int, char**); int batch(int, char**); int compare(int, char**); int extract(int, char**); int params(int, char**); } // namespace motis using namespace motis; int main(int ac, char** av) { auto const motis_version = std::string_view{MOTIS_VERSION}; if (ac > 1 && av[1] == "--help"sv) { fmt::println( "MOTIS {}\n\n" "Usage:\n" " --help print this help message\n" " --version print program version\n\n" "Commands:\n" " generate generate random queries and write them to a file\n" " batch run queries from a file\n" " params update query parameters for a batch file\n" " compare compare results from different batch runs\n" " config generate a config file from a list of input files\n" " import prepare input data, creates the data directory\n" " server starts a web server serving the API\n" " extract trips from a Itinerary to GTFS timetable\n" " pb2json convert GTFS-RT protobuf to JSON\n" " json2pb convert JSON to GTFS-RT protobuf\n" " shapes print shape segmentation for trips\n", motis_version); return 0; } else if (ac <= 1 || (ac >= 2 && av[1] == "--version"sv)) { fmt::println("{}", motis_version); return 0; } // Skip program argument, quit if no command. --ac; ++av; auto return_value = 0; // Execute command. auto const cmd = std::string_view{av[0]}; switch (cista::hash(cmd)) { case cista::hash("extract"): return_value = extract(ac, av); break; case cista::hash("generate"): return_value = generate(ac, av); break; case cista::hash("params"): return_value = params(ac, av); break; case cista::hash("batch"): return_value = batch(ac, av); break; case cista::hash("compare"): return_value = compare(ac, av); break; case cista::hash("config"): { auto paths = std::vector{}; for (auto i = 1; i != ac; ++i) { paths.push_back(std::string{av[i]}); } if (paths.empty() || paths.front() == "--help") { fmt::println( "usage: motis config [PATHS...]\n\n" "Generates a config.yml file in the current working " "directory.\n\n" "File type will be determined based on extension:\n" " - \".osm.pbf\" will be used as OpenStreetMap file.\n" " This enables street routing, geocoding and map tiles\n" " - the rest will be interpreted as static timetables.\n" " This enables transit routing." "\n\n" "Example: motis config germany-latest.osm.pbf " "germany.gtfs.zip\n"); return_value = paths.empty() ? 1 : 0; break; } std::ofstream{"config.yml"} << config::read_simple(paths) << "\n"; return_value = 0; break; } case cista::hash("server"): try { auto data_path = fs::path{"data"}; auto log_lvl = std::string{}; auto desc = po::options_description{"Server Options"}; add_data_path_opt(desc, data_path); add_log_level_opt(desc, log_lvl); add_help_opt(desc); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return_value = 0; break; } auto const c = config::read(data_path / "config.yml"); if ((return_value = set_log_level(c))) { break; } if (vm.count("log-level") && (return_value = set_log_level(std::move(log_lvl)))) { break; } return_value = server(data{data_path, c}, c, motis_version); } catch (std::exception const& e) { std::cerr << "unable to start server: " << e.what() << "\n"; return_value = 1; } break; case cista::hash("import"): { auto c = config{}; try { auto data_path = fs::path{"data"}; auto config_path = fs::path{"config.yml"}; auto filter_tasks = std::vector{}; auto desc = po::options_description{"Import Options"}; add_data_path_opt(desc, data_path); add_config_path_opt(desc, config_path); add_help_opt(desc); desc.add_options() // ("filter", boost::program_options::value>( &filter_tasks) ->composing(), "Filter tasks and only run selected import tasks. Tasks have to " "be active based on the configuration. Available tasks are:\n" " - osr (street_routing)\n" " - adr (geocoding/reverse_geocoding)\n" " - tt (timetable)\n" " - tbd (timetable.tb)\n" " - adr_extend (timetable+geocoding)\n" " - osr_footpath\n" " - matches (timetable+street_routing)\n" " - route_shapes (timetable.route_shapes)\n" " - tiles\n"); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return_value = 0; break; } c = config::read(config_path); if ((return_value = set_log_level(c))) { break; } auto const bars = utl::global_progress_bars{false}; import( c, std::move(data_path), filter_tasks.empty() ? std::nullopt : std::optional{filter_tasks}); return_value = 0; } catch (std::exception const& e) { fmt::println("unable to import: {}", e.what()); fmt::println("config:\n{}", fmt::streamed(c)); return_value = 1; } break; } case cista::hash("pb2json"): { try { auto p = fs::path{}; auto desc = po::options_description{"GTFS-RT Protobuf to JSON"}; desc.add_options() // ("path,p", boost::program_options::value(&p)->default_value(p), "Path to Protobuf GTFS-RT file"); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return_value = 0; break; } auto const protobuf = cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}; fmt::println("{}", nigiri::rt::protobuf_to_json(protobuf.view())); return_value = 0; } catch (std::exception const& e) { fmt::println("error: ", e.what()); return_value = 1; } break; } case cista::hash("json2pb"): { try { auto p = fs::path{}; auto desc = po::options_description{"GTFS-RT JSON to Protobuf"}; desc.add_options() // ("path,p", boost::program_options::value(&p)->default_value(p), "Path to GTFS-RT JSON file"); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return_value = 0; break; } auto const protobuf = cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}; fmt::println("{}", nigiri::rt::json_to_protobuf(protobuf.view())); return_value = 0; } catch (std::exception const& e) { fmt::println("error: ", e.what()); return_value = 1; } break; } case cista::hash("shapes"): { try { auto data_path = fs::path{"data"}; auto desc = po::options_description{"Analyze Shapes Options"}; add_trip_id_opt(desc); add_data_path_opt(desc, data_path); add_help_opt(desc); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return_value = 0; break; } if (vm.count("trip-id") == 0) { std::cerr << "missing trip-ids\n"; return_value = 2; break; } auto const c = config::read(data_path / "config.yml"); auto const ids = utl::to_vec( vm["trip-id"].as>(), [](auto const& trip_id) { // Set space_as_plus = true auto const opts = boost::urls::encoding_opts{true}; auto const decoded = boost::urls::decode_view{trip_id, opts}; return std::string{decoded.begin(), decoded.end()}; }); return_value = analyze_shapes(data{data_path, c}, ids) ? 0 : 1; } catch (std::exception const& e) { std::cerr << "unable to analyse shapes: " << e.what() << "\n"; return_value = 1; } break; } default: fmt::println( "Invalid command. Type motis --help for a list of commands."); return_value = 1; break; } google::protobuf::ShutdownProtobufLibrary(); return return_value; } ================================================ FILE: exe/params.cc ================================================ #include #include #include "boost/url/url.hpp" #include "utl/file_utils.h" #include "utl/parser/cstr.h" #include "./flags.h" namespace po = boost::program_options; namespace motis { int params(int ac, char** av) { auto params = std::string{}; auto in = std::string{}; auto out = std::string{}; auto desc = po::options_description{"Options"}; desc.add_options() // ("help", "Prints this help message") // ("params,p", po::value(¶ms)->default_value(params)) // ("in,i", po::value(&in)->default_value(in)) // ("out,o", po::value(&out)->default_value(out)); auto vm = parse_opt(ac, av, desc); if (vm.count("help")) { std::cout << desc << "\n"; return 0; } auto const override = boost::urls::url{params}; auto out_file = std::ofstream{out}; auto in_file = utl::open_file(in); auto line = std::optional{}; while ((line = utl::read_line(in_file))) { auto query = boost::urls::url{*line}; for (auto const& x : override.params()) { query.params().set(x.key, x.value); } out_file << query << "\n"; } return 0U; } } // namespace motis ================================================ FILE: include/motis/adr_extend_tt.h ================================================ #pragma once #include "date/tz.h" #include "nigiri/routing/clasz_mask.h" #include "nigiri/types.h" #include "motis/fwd.h" #include "motis/types.h" namespace motis { // Starts counting timetable places at the last OSM place. using adr_extra_place_idx_t = cista::strong; using tz_map_t = vector_map; struct adr_ext { vector_map location_place_; vector_map place_clasz_; vector_map place_importance_; }; date::time_zone const* get_tz(nigiri::timetable const&, adr_ext const*, tz_map_t const*, nigiri::location_idx_t); adr_ext adr_extend_tt(nigiri::timetable const&, adr::area_database const*, adr::typeahead&); } // namespace motis ================================================ FILE: include/motis/analyze_shapes.h ================================================ #include #include #include "motis/data.h" namespace motis { bool analyze_shapes(data const&, std::vector const& trip_ids); } // namespace motis ================================================ FILE: include/motis/box_rtree.h ================================================ #pragma once #include #include #include "cista/strong.h" #include "rtree.h" #include "geo/box.h" #include "geo/latlng.h" namespace motis { template concept BoxRtreePosHandler = requires(geo::box const& b, T const x, Fn&& f) { { std::forward(f)(b, x) }; }; template struct box_rtree { box_rtree() : rtree_{rtree_new()} {} ~box_rtree() { if (rtree_ != nullptr) { rtree_free(rtree_); } } box_rtree(box_rtree const& o) { if (this != &o) { if (rtree_ != nullptr) { rtree_free(rtree_); } rtree_ = rtree_clone(o.rtree_); } } box_rtree(box_rtree&& o) { if (this != &o) { rtree_ = o.rtree_; o.rtree_ = nullptr; } } box_rtree& operator=(box_rtree const& o) { if (this != &o) { if (rtree_ != nullptr) { rtree_free(rtree_); } rtree_ = rtree_clone(o.rtree_); } return *this; } box_rtree& operator=(box_rtree&& o) { if (this != &o) { rtree_ = o.rtree_; o.rtree_ = nullptr; } return *this; } void add(geo::box const& b, T const t) { auto const min_corner = b.min_.lnglat(); auto const max_corner = b.max_.lnglat(); rtree_insert( rtree_, min_corner.data(), max_corner.data(), reinterpret_cast(static_cast(cista::to_idx(t)))); } void remove(geo::box const& b, T const t) { auto const min_corner = b.min_.lnglat(); auto const max_corner = b.max_.lnglat(); rtree_delete( rtree_, min_corner.data(), max_corner.data(), reinterpret_cast(static_cast(cista::to_idx(t)))); } std::vector in_radius(geo::latlng const& x, double distance) const { auto ret = std::vector{}; in_radius(x, distance, [&](auto&& item) { ret.emplace_back(item); }); return ret; } template void in_radius(geo::latlng const& x, double distance, Fn&& fn) const { auto const rad_sq = distance * distance; auto const approx_distance_lng_degrees = geo::approx_distance_lng_degrees(x); find(geo::box{x, distance}, [&](geo::box const& box, T const item) { auto const closest = geo::latlng{std::clamp(x.lat(), box.min_.lat(), box.max_.lat()), std::clamp(x.lng(), box.min_.lng(), box.max_.lng())}; if (geo::approx_squared_distance(x, closest, approx_distance_lng_degrees) < rad_sq) { fn(item); } }); } template void find(geo::box const& b, Fn&& fn) const { auto const min = b.min_.lnglat(); auto const max = b.max_.lnglat(); rtree_search( rtree_, min.data(), max.data(), [](double const* min_corner, double const* max_corner, void const* item, void* udata) { if constexpr (BoxRtreePosHandler) { (*reinterpret_cast(udata))( geo::box{geo::latlng{min_corner[1], min_corner[0]}, geo::latlng{max_corner[1], max_corner[0]}}, T{static_cast>( reinterpret_cast(item))}); } else { (*reinterpret_cast(udata))(T{static_cast>( reinterpret_cast(item))}); } return true; }, &fn); } template void find(geo::latlng const& pos, Fn&& fn) const { return find(geo::box{pos, pos}, std::forward(fn)); } rtree* rtree_{nullptr}; }; } // namespace motis ================================================ FILE: include/motis/clog_redirect.h ================================================ #pragma once #include #include #include #include namespace motis { struct clog_redirect { explicit clog_redirect(char const* log_file_path); clog_redirect(clog_redirect const&) = delete; clog_redirect(clog_redirect&&) = delete; clog_redirect& operator=(clog_redirect const&) = delete; clog_redirect& operator=(clog_redirect&&) = delete; ~clog_redirect(); static void set_enabled(bool); private: std::ofstream sink_; std::unique_ptr sink_buf_; std::streambuf* backup_clog_{}; bool active_{}; std::mutex mutex_; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool enabled_; }; } // namespace motis ================================================ FILE: include/motis/compute_footpaths.h ================================================ #pragma once #include "cista/memory_holder.h" #include "osr/routing/profile.h" #include "osr/types.h" #include "motis/fwd.h" #include "motis/types.h" namespace motis { using elevator_footpath_map_t = hash_map< osr::node_idx_t, hash_set>>; struct routed_transfers_settings { osr::search_profile profile_; nigiri::profile_idx_t profile_idx_; double max_matching_distance_; bool extend_missing_{false}; std::chrono::seconds max_duration_; std::function is_candidate_{}; }; elevator_footpath_map_t compute_footpaths( osr::ways const&, osr::lookup const&, osr::platforms const&, nigiri::timetable&, osr::elevation_storage const*, bool update_coordinates, std::vector const& settings); } // namespace motis ================================================ FILE: include/motis/config.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include "cista/hashing.h" #include "utl/verify.h" namespace motis { using headers_t = std::map; struct config { friend std::ostream& operator<<(std::ostream&, config const&); static config read_simple(std::vector const& args); static config read(std::filesystem::path const&); static config read(std::string const&); void verify() const; void verify_input_files_exist() const; bool requires_rt_timetable_updates() const; bool shapes_debug_api_enabled() const; bool has_gbfs_feeds() const; bool has_prima() const; bool has_elevators() const; bool use_street_routing() const; bool operator==(config const&) const = default; struct server { bool operator==(server const&) const = default; std::string host_{"0.0.0.0"}; std::string port_{"8080"}; std::string web_folder_{"ui"}; unsigned n_threads_{0U}; std::optional data_attribution_link_{}; std::optional> lbs_{}; }; std::optional server_{}; std::optional osm_{}; struct tiles { bool operator==(tiles const&) const = default; std::filesystem::path profile_; std::optional coastline_{}; std::size_t db_size_{sizeof(void*) >= 8 ? 256ULL * 1024ULL * 1024ULL * 1024ULL : 256U * 1024U * 1024U}; std::size_t flush_threshold_{100'000}; }; std::optional tiles_{}; struct timetable { struct dataset { struct rt { bool operator==(rt const&) const = default; cista::hash_t hash() const noexcept { return cista::build_hash(url_, headers_); } std::string url_; std::optional headers_{}; enum struct protocol { gtfsrt, auser, siri, siri_json }; protocol protocol_{protocol::gtfsrt}; }; bool operator==(dataset const&) const = default; std::string path_; std::optional script_{}; bool default_bikes_allowed_{false}; bool default_cars_allowed_{false}; bool extend_calendar_{false}; std::optional> clasz_bikes_allowed_{}; std::optional> clasz_cars_allowed_{}; std::optional> rt_{}; std::optional default_timezone_{}; }; struct shapes_debug { bool operator==(shapes_debug const&) const = default; std::filesystem::path path_; std::optional> trips_{}; std::optional> route_ids_{}; std::optional> route_indices_{}; bool all_{false}; bool all_with_beelines_{false}; unsigned slow_{0U}; }; struct route_shapes { enum class mode { all, missing }; bool operator==(route_shapes const&) const = default; mode mode_{mode::all}; bool cache_reuse_old_osm_data_{false}; std::size_t cache_db_size_{sizeof(void*) >= 8 ? 256ULL * 1024ULL * 1024ULL * 1024ULL : 256U * 1024U * 1024U}; std::optional> clasz_{}; unsigned max_stops_{0U}; unsigned n_threads_{0U}; bool debug_api_{false}; std::optional debug_{}; }; bool operator==(timetable const&) const = default; std::string first_day_{"TODAY"}; std::uint16_t num_days_{365U}; bool tb_{false}; bool railviz_{true}; bool with_shapes_{true}; bool adjust_footpaths_{true}; bool merge_dupes_intra_src_{false}; bool merge_dupes_inter_src_{false}; unsigned link_stop_distance_{100U}; unsigned update_interval_{60}; unsigned http_timeout_{30}; bool canned_rt_{false}; bool incremental_rt_update_{false}; bool use_osm_stop_coordinates_{false}; bool extend_missing_footpaths_{false}; std::uint16_t max_footpath_length_{15}; double max_matching_distance_{25.0}; double preprocess_max_matching_distance_{250.0}; std::optional default_timezone_{}; std::map datasets_{}; std::optional assistance_times_{}; std::optional route_shapes_{}; }; std::optional timetable_{}; struct gbfs { bool operator==(gbfs const&) const = default; struct ttl { bool operator==(ttl const&) const = default; std::optional> default_{}; std::optional> overwrite_{}; }; struct restrictions { bool operator==(restrictions const&) const = default; bool ride_start_allowed_{true}; bool ride_end_allowed_{true}; bool ride_through_allowed_{true}; std::optional station_parking_{}; std::optional return_constraint_{}; }; struct oauth_settings { bool operator==(oauth_settings const&) const = default; std::string token_url_; std::string client_id_; std::string client_secret_; std::optional headers_{}; std::optional expires_in_; }; struct feed { bool operator==(feed const&) const = default; std::string url_; std::optional headers_{}; std::optional oauth_{}; std::optional< std::variant>> group_{}; std::optional< std::variant>> color_{}; std::optional ttl_{}; }; struct group { bool operator==(group const&) const = default; std::optional name_{}; std::optional color_{}; std::optional url_{}; }; std::map feeds_{}; std::map groups_{}; std::map default_restrictions_{}; unsigned update_interval_{60}; unsigned http_timeout_{30}; unsigned cache_size_{50}; std::optional proxy_{}; std::optional ttl_{}; }; std::optional gbfs_{}; struct prima { bool operator==(prima const&) const = default; std::string url_{}; std::optional bounds_{}; std::optional ride_sharing_bounds_{}; }; std::optional prima_{}; struct elevators { bool operator==(elevators const&) const = default; std::optional url_{}; std::optional init_{}; std::optional osm_mapping_{}; unsigned http_timeout_{10}; std::optional headers_{}; }; unsigned n_threads() const; std::optional const& get_elevators() const; std::variant> elevators_{false}; struct street_routing { bool operator==(street_routing const&) const = default; std::optional elevation_data_dir_; }; std::optional get_street_routing() const; std::variant> street_routing_{false}; struct limits { bool operator==(limits const&) const = default; unsigned stoptimes_max_results_{256U}; unsigned plan_max_results_{256U}; unsigned plan_max_search_window_minutes_{5760U}; unsigned stops_max_results_{2048U}; unsigned onetomany_max_many_{128U}; unsigned onetoall_max_results_{65535U}; unsigned onetoall_max_travel_minutes_{90U}; unsigned routing_max_timeout_seconds_{90U}; unsigned gtfsrt_expose_max_trip_updates_{100U}; unsigned street_routing_max_prepost_transit_seconds_{3600U}; unsigned street_routing_max_direct_seconds_{21600U}; unsigned geocode_max_suggestions_{10U}; unsigned reverse_geocode_max_results_{5U}; }; limits get_limits() const { return limits_.value_or(limits{}); } std::optional limits_{}; struct logging { bool operator==(logging const&) const = default; std::optional log_level_{}; }; std::optional logging_{}; bool osr_footpath_{false}; bool geocoding_{false}; bool reverse_geocoding_{false}; }; } // namespace motis ================================================ FILE: include/motis/constants.h ================================================ #pragma once namespace motis { // search radius for neighbors to route to [meters] constexpr auto const kMaxDistance = 2000; // max distance from start/destination coordinate to way segment [meters] constexpr auto const kMaxMatchingDistance = 25.0; constexpr auto const kMaxWheelchairMatchingDistance = 8.0; // max distance from gbfs vehicle/station to way segment [meters] constexpr auto const kMaxGbfsMatchingDistance = 100.0; // distance between location in timetable and OSM platform coordinate [meters] constexpr auto const kMaxAdjust = 200; // multiplier for transfer times constexpr auto const kTransferTimeMultiplier = 1.5F; // footpaths of public transport locations around this distance // are updated on elevator status changes [meters] constexpr auto const kElevatorUpdateRadius = 1000.; } // namespace motis ================================================ FILE: include/motis/ctx_data.h ================================================ #pragma once #include "ctx/op_id.h" #include "ctx/operation.h" namespace motis { struct ctx_data { void transition(ctx::transition, ctx::op_id, ctx::op_id) {} }; } // namespace motis ================================================ FILE: include/motis/ctx_exec.h ================================================ #pragma once #include #include "boost/asio/io_context.hpp" #include "boost/asio/post.hpp" #include "ctx/scheduler.h" #include "motis/ctx_data.h" namespace motis { struct ctx_exec { ctx_exec(boost::asio::io_context& io, ctx::scheduler& sched) : io_{io}, sched_{sched} {} void exec(auto&& f, net::web_server::http_res_cb_t cb) { sched_.post_void_io( ctx_data{}, [&, f = std::move(f), cb = std::move(cb)]() mutable { try { auto res = std::make_shared(f()); boost::asio::post( io_, [cb = std::move(cb), res = std::move(res)]() mutable { cb(std::move(*res)); }); } catch (...) { std::cerr << "UNEXPECTED EXCEPTION\n"; auto str = net::web_server::string_res_t{ boost::beast::http::status::internal_server_error, 11}; str.body() = "error"; str.prepare_payload(); auto res = std::make_shared(str); boost::asio::post( io_, [cb = std::move(cb), res = std::move(res)]() mutable { cb(std::move(*res)); }); } }, CTX_LOCATION); } boost::asio::io_context& io_; ctx::scheduler& sched_; }; } // namespace motis ================================================ FILE: include/motis/data.h ================================================ #pragma once #include #include "cista/memory_holder.h" #include "date/date.h" #include "nigiri/rt/vdv_aus.h" #include "nigiri/types.h" #include "osr/types.h" #include "motis-api/motis-api.h" #include "motis/adr_extend_tt.h" #include "motis/config.h" #include "motis/elevators/parse_elevator_id_osm_mapping.h" #include "motis/fwd.h" #include "motis/gbfs/data.h" #include "motis/match_platforms.h" #include "motis/rt/auser.h" #include "motis/types.h" namespace motis { struct elevators; template struct point_rtree; template using ptr = std::unique_ptr; struct rt { rt(); rt(ptr&&, ptr&&, ptr&&); ~rt(); ptr rtt_; ptr railviz_rt_; ptr e_; }; struct data { data(std::filesystem::path); data(std::filesystem::path, config const&); ~data(); data(data const&) = delete; data& operator=(data const&) = delete; data(data&&); data& operator=(data&&); friend std::ostream& operator<<(std::ostream&, data const&); void load_osr(); void load_tt(std::filesystem::path const&); void load_flex_areas(); void load_shapes(); void load_railviz(); void load_tbd(); void load_geocoder(); void load_matches(); void load_way_matches(); void load_reverse_geocoder(); void load_tiles(); void load_auser_updater(std::string_view, config::timetable::dataset const&); void init_rtt(date::sys_days = std::chrono::time_point_cast( std::chrono::system_clock::now())); auto cista_members() { // !!! Remember to add all new members !!! return std::tie(config_, motis_version_, initial_response_, t_, adr_ext_, f_, tz_, r_, tc_, w_, pl_, l_, elevations_, tt_, tbd_, tags_, location_rtree_, elevator_nodes_, elevator_osm_mapping_, shapes_, railviz_static_, matches_, way_matches_, rt_, gbfs_, odm_bounds_, ride_sharing_bounds_, flex_areas_, metrics_, auser_); } std::filesystem::path path_; config config_; std::string_view motis_version_; api::initial_response initial_response_; cista::wrapped t_; cista::wrapped adr_ext_; ptr f_; ptr> tz_; ptr r_; ptr tc_; ptr w_; ptr pl_; ptr l_; ptr elevations_; cista::wrapped tt_; cista::wrapped tbd_; cista::wrapped tags_; ptr> location_rtree_; ptr> elevator_nodes_; ptr elevator_osm_mapping_; ptr shapes_; ptr railviz_static_; cista::wrapped> matches_; ptr way_matches_; ptr tiles_; std::shared_ptr rt_{std::make_shared()}; std::shared_ptr gbfs_{}; ptr odm_bounds_; ptr ride_sharing_bounds_; ptr flex_areas_; ptr metrics_; ptr> auser_; }; } // namespace motis ================================================ FILE: include/motis/direct_filter.h ================================================ #pragma once #include #include "nigiri/routing/journey.h" #include "motis-api/motis-api.h" namespace motis { void direct_filter(std::vector const& direct, std::vector&); } // namespace motis ================================================ FILE: include/motis/elevators/elevators.h ================================================ #pragma once #include "motis/elevators/match_elevator.h" #include "motis/elevators/parse_elevator_id_osm_mapping.h" #include "motis/fwd.h" #include "motis/point_rtree.h" namespace motis { struct elevators { elevators(osr::ways const&, elevator_id_osm_mapping_t const*, hash_set const&, vector_map&&); vector_map elevators_; point_rtree elevators_rtree_; osr::bitvec blocked_; }; } // namespace motis ================================================ FILE: include/motis/elevators/get_state_changes.h ================================================ #pragma once #include #include #include "fmt/ranges.h" #include "nigiri/common/interval.h" #include "utl/enumerate.h" #include "utl/generator.h" #include "utl/to_vec.h" #include "utl/verify.h" namespace motis { template struct state_change { friend bool operator==(state_change const&, state_change const&) = default; Time valid_from_; bool state_; }; template std::vector> intervals_to_state_changes( std::vector> const& iv, bool const status) { using Duration = typename Time::duration; auto ret = std::vector>{}; if (iv.empty()) { ret.push_back({Time{Duration{0}}, status}); } else { ret.push_back({Time{Duration{0}}, Default}); for (auto const& i : iv) { ret.push_back({i.from_, !Default}); ret.push_back({i.to_, Default}); } } return ret; } template static utl::generator>> get_state_changes( std::vector>> const& c) { using It = std::vector>::const_iterator; struct range { bool is_finished() const { return curr_ == end_; } bool state_{false}; It curr_, end_; }; auto its = utl::to_vec(c, [](auto&& v) { utl::verify(!v.empty(), "empty state vector not allowed"); return range{v[0].state_, v.begin(), v.end()}; }); auto const all_finished = [&]() { return std::ranges::all_of(its, [&](auto&& r) { return r.is_finished(); }); }; auto const next = [&]() -> range& { auto const it = std::ranges::min_element(its, [&](auto&& a, auto&& b) { if (a.curr_ == a.end_) { return false; } else if (b.curr_ == b.end_) { return true; } else { return a.curr_->valid_from_ < b.curr_->valid_from_; } }); assert(it != end(its)); return *it; }; auto const get_state = [&]() -> std::vector { auto s = std::vector(its.size()); for (auto const [i, r] : utl::enumerate(its)) { s[i] = r.state_; } return s; }; auto pred_t = std::optional>>{}; while (!all_finished()) { auto& n = next(); auto const t = n.curr_->valid_from_; n.state_ = n.curr_->state_; ++n.curr_; auto const state = std::pair{t, get_state()}; if (!pred_t.has_value()) { pred_t = state; continue; } if (pred_t->first != state.first) { co_yield *pred_t; } pred_t = state; } if (pred_t.has_value()) { co_yield *pred_t; } co_return; } } // namespace motis ================================================ FILE: include/motis/elevators/match_elevator.h ================================================ #pragma once #include "osr/types.h" #include "motis/elevators/parse_elevator_id_osm_mapping.h" #include "motis/fwd.h" #include "motis/point_rtree.h" #include "motis/types.h" namespace motis { point_rtree create_elevator_rtree( vector_map const&); osr::hash_set get_elevator_nodes(osr::ways const&); elevator_idx_t match_elevator(point_rtree const&, vector_map const&, osr::ways const&, osr::node_idx_t); osr::bitvec get_blocked_elevators( osr::ways const&, elevator_id_osm_mapping_t const*, vector_map const&, point_rtree const&, osr::hash_set const&); } // namespace motis ================================================ FILE: include/motis/elevators/parse_elevator_id_osm_mapping.h ================================================ #pragma once #include #include #include #include "motis/types.h" namespace motis { using elevator_id_osm_mapping_t = hash_map; elevator_id_osm_mapping_t parse_elevator_id_osm_mapping(std::string_view); elevator_id_osm_mapping_t parse_elevator_id_osm_mapping( std::filesystem::path const&); } // namespace motis ================================================ FILE: include/motis/elevators/parse_fasta.h ================================================ #pragma once #include #include #include "boost/json/object.hpp" #include "motis/types.h" namespace motis { std::vector> parse_out_of_service( boost::json::object const&); vector_map parse_fasta(std::string_view); vector_map parse_fasta(std::filesystem::path const&); } // namespace motis ================================================ FILE: include/motis/elevators/parse_siri_fm.h ================================================ #pragma once #include #include #include "motis/types.h" namespace motis { vector_map parse_siri_fm(std::string_view); vector_map parse_siri_fm( std::filesystem::path const&); } // namespace motis ================================================ FILE: include/motis/elevators/update_elevators.h ================================================ #pragma once #include #include #include "motis/elevators/elevators.h" #include "motis/fwd.h" namespace motis { std::unique_ptr update_elevators(config const&, data const&, std::string_view fasta_json, nigiri::rt_timetable&); } // namespace motis ================================================ FILE: include/motis/endpoints/adr/filter_conv.h ================================================ #pragma once #include "adr/types.h" #include "motis-api/motis-api.h" namespace motis { adr::filter_type to_filter_type( std::optional const&); } // namespace motis ================================================ FILE: include/motis/endpoints/adr/geocode.h ================================================ #pragma once #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis::ep { struct geocode { api::geocode_response operator()(boost::urls::url_view const& url) const; config const& config_; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; nigiri::timetable const* tt_; tag_lookup const* tags_; adr::typeahead const& t_; adr::formatter const& f_; adr::cache& cache_; adr_ext const* ae_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/adr/reverse_geocode.h ================================================ #pragma once #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis::ep { struct reverse_geocode { api::reverseGeocode_response operator()( boost::urls::url_view const& url) const; config const& config_; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; nigiri::timetable const* tt_; tag_lookup const* tags_; adr::typeahead const& t_; adr::formatter const& f_; adr::reverse const& r_; adr_ext const* ae_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/adr/suggestions_to_response.h ================================================ #pragma once #include "adr/adr.h" #include "adr/types.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/types.h" namespace motis { api::geocode_response suggestions_to_response( adr::typeahead const&, adr::formatter const&, adr_ext const*, nigiri::timetable const*, tag_lookup const*, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, basic_string const& lang_indices, std::vector const& token_pos, std::vector const&); } // namespace motis ================================================ FILE: include/motis/endpoints/elevators.h ================================================ #pragma once #include "boost/json/value.hpp" #include "nigiri/types.h" #include "motis/elevators/elevators.h" #include "motis/fwd.h" namespace motis::ep { struct elevators { boost::json::value operator()(boost::json::value const&) const; std::shared_ptr const& rt_; osr::ways const& w_; osr::lookup const& l_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/graph.h ================================================ #pragma once #include "boost/json/value.hpp" #include "motis/fwd.h" namespace motis::ep { struct graph { boost::json::value operator()(boost::json::value const&) const; osr::ways const& w_; osr::lookup const& l_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/gtfsrt.h ================================================ #pragma once #include "net/web_server/query_router.h" #include "motis/fwd.h" namespace motis::ep { struct gtfsrt { net::reply operator()(net::route_request const&, bool) const; config const& config_; nigiri::timetable const* tt_; tag_lookup const* tags_; std::shared_ptr const& rt_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/initial.h ================================================ #pragma once #include "boost/url/url.hpp" #include "motis-api/motis-api.h" #include "motis/fwd.h" namespace motis::ep { api::initial_response get_initial_response(data const&); struct initial { api::initial_response operator()(boost::urls::url_view const&) const; api::initial_response const& response_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/levels.h ================================================ #pragma once #include "boost/url/url_view.hpp" #include "motis-api/motis-api.h" #include "motis/fwd.h" namespace motis::ep { struct levels { api::levels_response operator()(boost::urls::url_view const&) const; osr::ways const& w_; osr::lookup const& l_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/flex_locations.h ================================================ #pragma once #include "boost/json/value.hpp" #include "boost/url/url_view.hpp" #include "nigiri/types.h" #include "motis/fwd.h" #include "motis/point_rtree.h" namespace motis::ep { struct flex_locations { boost::json::value operator()(boost::urls::url_view const&) const; tag_lookup const& tags_; nigiri::timetable const& tt_; point_rtree const& loc_rtree_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/rental.h ================================================ #pragma once #include #include "boost/url/url_view.hpp" #include "motis-api/motis-api.h" #include "motis/fwd.h" namespace motis::ep { struct rental { api::rentals_response operator()(boost::urls::url_view const&) const; std::shared_ptr const& gbfs_; nigiri::timetable const* tt_; tag_lookup const* tags_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/route_details.h ================================================ #pragma once #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis::ep { struct route_details { api::routeDetails_response operator()(boost::urls::url_view const&) const; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; tag_lookup const& tags_; nigiri::timetable const& tt_; std::shared_ptr const& rt_; nigiri::shapes_storage const* shapes_; railviz_static_index const& static_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/routes.h ================================================ #pragma once #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis::ep { struct routes { api::routes_response operator()(boost::urls::url_view const&) const; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; tag_lookup const& tags_; nigiri::timetable const& tt_; std::shared_ptr const& rt_; nigiri::shapes_storage const* shapes_; railviz_static_index const& static_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/shapes_debug.h ================================================ #pragma once #include "net/web_server/query_router.h" #include "motis/config.h" #include "motis/fwd.h" namespace motis::ep { struct shapes_debug { net::reply operator()(net::route_request const&, bool) const; config const& c_; osr::ways const* w_; osr::lookup const* l_; nigiri::timetable const* tt_; tag_lookup const* tags_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/stops.h ================================================ #pragma once #include "boost/url/url_view.hpp" #include "nigiri/types.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/point_rtree.h" namespace motis::ep { struct stops { api::stops_response operator()(boost::urls::url_view const&) const; config const& config_; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; point_rtree const& loc_rtree_; tag_lookup const& tags_; nigiri::timetable const& tt_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/map/trips.h ================================================ #pragma once #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis::ep { struct trips { api::trips_response operator()(boost::urls::url_view const&) const; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; tag_lookup const& tags_; nigiri::timetable const& tt_; std::shared_ptr const& rt_; nigiri::shapes_storage const* shapes_; railviz_static_index const& static_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/matches.h ================================================ #pragma once #include "boost/json/value.hpp" #include "nigiri/types.h" #include "motis/fwd.h" #include "motis/point_rtree.h" namespace motis::ep { struct matches { boost::json::value operator()(boost::json::value const&) const; point_rtree const& loc_rtree_; tag_lookup const& tags_; nigiri::timetable const& tt_; osr::ways const& w_; osr::lookup const& l_; osr::platforms const& pl_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/metrics.h ================================================ #pragma once #include #include "net/web_server/query_router.h" #include "motis/fwd.h" #include "motis/metrics_registry.h" namespace motis::ep { struct metrics { net::reply operator()(net::route_request const&, bool) const; nigiri::timetable const* tt_; tag_lookup const* tags_; std::shared_ptr const& rt_; metrics_registry* metrics_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/ojp.h ================================================ #pragma once #include #include "net/web_server/query_router.h" #include "motis/endpoints/adr/geocode.h" #include "motis/endpoints/map/stops.h" #include "motis/endpoints/routing.h" #include "motis/endpoints/stop_times.h" #include "motis/endpoints/trip.h" namespace motis::ep { struct ojp { net::reply operator()(net::route_request const&, bool) const; std::optional routing_ep_; std::optional geocoding_ep_; std::optional stops_ep_; std::optional stop_times_ep_; std::optional trip_ep_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/one_to_all.h ================================================ #pragma once #include "boost/url/url_view.hpp" #include "motis-api/motis-api.h" #include "motis/data.h" #include "motis/fwd.h" namespace motis::ep { struct one_to_all { api::Reachable operator()(boost::urls::url_view const&) const; config const& config_; osr::ways const* w_; osr::lookup const* l_; osr::platforms const* pl_; osr::elevation_storage const* elevations_; nigiri::timetable const& tt_; std::shared_ptr const& rt_; tag_lookup const& tags_; flex::flex_areas const* fa_; point_rtree const* loc_tree_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; way_matches_storage const* way_matches_; std::shared_ptr const& gbfs_; metrics_registry* metrics_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/one_to_many.h ================================================ #pragma once #include #include "boost/url/url_view.hpp" #include "utl/to_vec.h" #include "net/bad_request_exception.h" #include "nigiri/types.h" #include "osr/location.h" #include "osr/routing/route.h" #include "osr/types.h" #include "motis-api/motis-api.h" #include "motis/data.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/metrics_registry.h" #include "motis/osr/parameters.h" #include "motis/parse_location.h" #include "motis/place.h" #include "motis/point_rtree.h" namespace motis::ep { api::oneToMany_response one_to_many_direct( config const&, osr::ways const&, osr::lookup const&, api::ModeEnum, osr::location const& one, std::vector const& many, double max_direct_time, double max_matching_distance, osr::direction, osr_parameters const&, api::PedestrianProfileEnum, api::ElevationCostsEnum, osr::elevation_storage const*, bool with_distance); template api::oneToMany_response one_to_many_handle_request( config const& config, Params const& query, osr::ways const& w, osr::lookup const& l, osr::elevation_storage const* elevations, metrics_registry* metrics) { metrics->one_to_many_requests_.Increment(); // required field with default value, not std::optional static_assert(std::is_same_v); auto const one = parse_location(query.one_, ';'); utl::verify( one.has_value(), "{} is not a valid geo coordinate", query.one_); auto const many = utl::to_vec(query.many_, [](auto&& x) { auto const y = parse_location(x, ';'); utl::verify( y.has_value(), "{} is not a valid geo coordinate", x); return *y; }); return one_to_many_direct( config, w, l, query.mode_, *one, many, query.max_, query.maxMatchingDistance_, query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward, get_osr_parameters(query), api::PedestrianProfileEnum::FOOT, query.elevationCosts_, elevations, query.withDistance_); } template api::OneToManyIntermodalResponse run_one_to_many_intermodal( Endpoint const&, Query const&, place_t const& one, std::vector const& many); struct one_to_many { api::oneToMany_response operator()(boost::urls::url_view const&) const; config const& config_; osr::ways const& w_; osr::lookup const& l_; osr::elevation_storage const* elevations_; metrics_registry* metrics_; }; struct one_to_many_intermodal { api::OneToManyIntermodalResponse operator()( boost::urls::url_view const&) const; config const& config_; osr::ways const* w_; osr::lookup const* l_; osr::platforms const* pl_; osr::elevation_storage const* elevations_; nigiri::timetable const& tt_; std::shared_ptr const& rt_; tag_lookup const& tags_; flex::flex_areas const* fa_; point_rtree const* loc_tree_; platform_matches_t const* matches_; way_matches_storage const* way_matches_; std::shared_ptr const& gbfs_; metrics_registry* metrics_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/one_to_many_post.h ================================================ #pragma once #include #include "nigiri/types.h" #include "motis-api/motis-api.h" #include "motis/data.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/point_rtree.h" namespace motis::ep { struct one_to_many_post { api::oneToManyPost_response operator()( motis::api::OneToManyParams const&) const; config const& config_; osr::ways const& w_; osr::lookup const& l_; osr::elevation_storage const* elevations_; metrics_registry* metrics_; }; struct one_to_many_intermodal_post { api::OneToManyIntermodalResponse operator()( api::OneToManyIntermodalParams const&) const; config const& config_; osr::ways const* w_; osr::lookup const* l_; osr::platforms const* pl_; osr::elevation_storage const* elevations_; nigiri::timetable const& tt_; std::shared_ptr const& rt_; tag_lookup const& tags_; flex::flex_areas const* fa_; point_rtree const* loc_tree_; platform_matches_t const* matches_; way_matches_storage const* way_matches_; std::shared_ptr const& gbfs_; metrics_registry* metrics_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/osr_routing.h ================================================ #pragma once #include "boost/json/value.hpp" #include "motis/elevators/elevators.h" #include "motis/fwd.h" namespace motis::ep { struct osr_routing { boost::json::value operator()(boost::json::value const&) const; osr::ways const& w_; osr::lookup const& l_; std::shared_ptr const& rt_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/platforms.h ================================================ #pragma once #include "boost/json/value.hpp" #include "motis/fwd.h" namespace motis::ep { struct platforms { boost::json::value operator()(boost::json::value const&) const; osr::ways const& w_; osr::lookup const& l_; osr::platforms const& pl_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/routing.h ================================================ #pragma once #include #include #include #include #include "boost/thread/tss.hpp" #include "osr/types.h" #include "nigiri/routing/clasz_mask.h" #include "nigiri/routing/raptor/raptor_state.h" #include "nigiri/routing/raptor_search.h" #include "motis-api/motis-api.h" #include "motis/elevators/elevators.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" #include "motis/place.h" namespace motis::ep { constexpr auto const kInfinityDuration = nigiri::duration_t{std::numeric_limits::max()}; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern boost::thread_specific_ptr> blocked; using stats_map_t = std::map; nigiri::interval shrink( bool keep_late, std::size_t max_size, nigiri::interval search_interval, std::vector& journeys); std::vector station_start(nigiri::location_idx_t); std::vector get_via_stops( nigiri::timetable const&, tag_lookup const&, std::optional> const& vias, std::vector const& times, bool reverse); std::vector deduplicate(std::vector); void remove_slower_than_fastest_direct(nigiri::routing::query&); struct routing { api::plan_response operator()(boost::urls::url_view const&) const; std::vector get_offsets( nigiri::rt_timetable const*, place_t const&, osr::direction, std::vector const&, std::optional> const&, std::optional> const&, std::optional> const& rental_providers, std::optional> const& rental_provider_groups, bool ignore_rental_return_constraints, osr_parameters const&, api::PedestrianProfileEnum, api::ElevationCostsEnum, std::chrono::seconds max, double max_matching_distance, gbfs::gbfs_routing_data&, stats_map_t& stats) const; nigiri::hash_map> get_td_offsets(nigiri::rt_timetable const* rtt, elevators const*, place_t const&, osr::direction, std::vector const&, osr_parameters const&, api::PedestrianProfileEnum, api::ElevationCostsEnum, double max_matching_distance, std::chrono::seconds max, nigiri::routing::start_time_t const&, stats_map_t& stats) const; std::pair, nigiri::duration_t> route_direct( elevators const*, gbfs::gbfs_routing_data&, nigiri::lang_t const&, api::Place const& from, api::Place const& to, std::vector const&, std::optional> const&, std::optional> const&, std::optional> const& rental_providers, std::optional> const& rental_provider_groups, bool ignore_rental_return_constraints, nigiri::unixtime_t time, bool arrive_by, osr_parameters const&, api::PedestrianProfileEnum, api::ElevationCostsEnum, std::chrono::seconds max, double max_matching_distance, double fastest_direct_factor, bool detailed_legs, unsigned api_version) const; config const& config_; osr::ways const* w_; osr::lookup const* l_; osr::platforms const* pl_; osr::elevation_storage const* elevations_; nigiri::timetable const* tt_; nigiri::routing::tb::tb_data const* tbd_; tag_lookup const* tags_; point_rtree const* loc_tree_; flex::flex_areas const* fa_; platform_matches_t const* matches_; way_matches_storage const* way_matches_; std::shared_ptr const& rt_; nigiri::shapes_storage const* shapes_; std::shared_ptr const& gbfs_; adr_ext const* ae_; tz_map_t const* tz_; odm::bounds const* odm_bounds_; odm::ride_sharing_bounds const* ride_sharing_bounds_; metrics_registry* metrics_; }; bool is_intermodal(routing const&, place_t const&); nigiri::routing::location_match_mode get_match_mode(routing const&, place_t const&); } // namespace motis::ep ================================================ FILE: include/motis/endpoints/stop_times.h ================================================ #pragma once #include "nigiri/types.h" #include "osr/types.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/point_rtree.h" #include "motis/types.h" namespace motis::ep { struct stop_times { api::stoptimes_response operator()(boost::urls::url_view const&) const; config const& config_; osr::ways const* w_; osr::platforms const* pl_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; point_rtree const& loc_rtree_; nigiri::timetable const& tt_; tag_lookup const& tags_; std::shared_ptr const& rt_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/tiles.h ================================================ #pragma once #include "net/web_server/query_router.h" #include "motis/fwd.h" namespace motis::ep { struct tiles { net::reply operator()(net::route_request const&, bool) const; tiles_data& tiles_data_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/transfers.h ================================================ #pragma once #include "boost/url/url_view.hpp" #include "nigiri/types.h" #include "osr/types.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/point_rtree.h" #include "motis/types.h" namespace motis::ep { struct transfers { api::transfers_response operator()(boost::urls::url_view const&) const; config const& c_; tag_lookup const& tags_; nigiri::timetable const& tt_; osr::ways const& w_; osr::lookup const& l_; osr::platforms const& pl_; point_rtree const& loc_rtree_; platform_matches_t const& matches_; std::shared_ptr rt_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/trip.h ================================================ #pragma once #include "boost/url/url_view.hpp" #include "motis-api/motis-api.h" #include "motis/elevators/elevators.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis::ep { struct trip { api::Itinerary operator()(boost::urls::url_view const&) const; config const& config_; osr::ways const* w_; osr::lookup const* l_; osr::platforms const* pl_; platform_matches_t const* matches_; nigiri::timetable const& tt_; nigiri::shapes_storage const* shapes_; adr_ext const* ae_; tz_map_t const* tz_; tag_lookup const& tags_; point_rtree const& loc_tree_; std::shared_ptr const& rt_; }; } // namespace motis::ep ================================================ FILE: include/motis/endpoints/update_elevator.h ================================================ #pragma once #include "boost/json/value.hpp" #include "utl/init_from.h" #include "nigiri/types.h" #include "osr/types.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/point_rtree.h" #include "motis/types.h" namespace motis::ep { struct update_elevator { boost::json::value operator()(boost::json::value const&) const; config const& c_; nigiri::timetable const& tt_; osr::ways const& w_; osr::lookup const& l_; osr::platforms const& pl_; point_rtree const& loc_rtree_; hash_set const& elevator_nodes_; elevator_id_osm_mapping_t const* elevator_ids_; platform_matches_t const& matches_; std::shared_ptr& rt_; }; } // namespace motis::ep ================================================ FILE: include/motis/flex/flex.h ================================================ #pragma once #include "osr/location.h" #include "osr/routing/profile.h" #include "osr/types.h" #include "nigiri/routing/query.h" #include "motis/flex/mode_id.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" namespace motis::flex { using flex_routings_t = hash_map, std::vector>; osr::sharing_data prepare_sharing_data(nigiri::timetable const&, osr::ways const&, osr::lookup const&, osr::platforms const*, flex_areas const&, platform_matches_t const*, mode_id, osr::direction, flex_routing_data&); bool is_in_flex_stop(nigiri::timetable const&, osr::ways const&, flex_areas const&, flex_routing_data const&, nigiri::flex_stop_t const&, osr::node_idx_t); flex_routings_t get_flex_routings(nigiri::timetable const&, point_rtree const&, nigiri::routing::start_time_t, geo::latlng const&, osr::direction, std::chrono::seconds max); void add_flex_td_offsets(osr::ways const&, osr::lookup const&, osr::platforms const*, platform_matches_t const*, way_matches_storage const*, nigiri::timetable const&, flex_areas const&, point_rtree const&, nigiri::routing::start_time_t, osr::location const&, osr::direction, std::chrono::seconds max, double const max_matching_distance, osr_parameters const&, flex_routing_data&, nigiri::routing::td_offsets_t&, std::map& stats); } // namespace motis::flex ================================================ FILE: include/motis/flex/flex_areas.h ================================================ #pragma once #include #include "tg.h" #include "osr/types.h" #include "nigiri/types.h" #include "motis/fwd.h" #include "motis/gbfs/compression.h" #include "motis/types.h" namespace motis::flex { struct flex_areas { explicit flex_areas(nigiri::timetable const&, osr::ways const&, osr::lookup const&); ~flex_areas(); void add_area(nigiri::flex_area_idx_t, osr::bitvec&, osr::bitvec& tmp) const; bool is_in_area(nigiri::flex_area_idx_t, geo::latlng const&) const; vector_map area_nodes_; vector_map idx_; }; } // namespace motis::flex ================================================ FILE: include/motis/flex/flex_output.h ================================================ #pragma once #include "motis/flex/flex_routing_data.h" #include "motis/flex/mode_id.h" #include "motis/osr/street_routing.h" namespace motis::flex { std::string_view get_flex_stop_name(nigiri::timetable const&, nigiri::lang_t const&, nigiri::flex_stop_t const&); std::string_view get_flex_id(nigiri::timetable const&, nigiri::flex_stop_t const&); struct flex_output : public output { flex_output(osr::ways const&, osr::lookup const&, osr::platforms const*, platform_matches_t const*, adr_ext const*, tz_map_t const*, tag_lookup const&, nigiri::timetable const&, flex_areas const&, mode_id); ~flex_output() override; api::ModeEnum get_mode() const override; osr::search_profile get_profile() const override; bool is_time_dependent() const override; transport_mode_t get_cache_key() const override; osr::sharing_data const* get_sharing_data() const override; void annotate_leg(nigiri::lang_t const&, osr::node_idx_t, osr::node_idx_t, api::Leg&) const override; api::Place get_place( nigiri::lang_t const&, osr::node_idx_t, std::optional const& fallback_tz) const override; std::size_t get_additional_node_idx(osr::node_idx_t) const; private: osr::ways const& w_; osr::platforms const* pl_; platform_matches_t const* matches_; adr_ext const* ae_; tz_map_t const* tz_; nigiri::timetable const& tt_; tag_lookup const& tags_; flex_areas const& fa_; flex::flex_routing_data flex_routing_data_; osr::sharing_data sharing_data_; mode_id mode_id_; }; } // namespace motis::flex ================================================ FILE: include/motis/flex/flex_routing_data.h ================================================ #pragma once #include #include "osr/routing/additional_edge.h" #include "osr/routing/sharing_data.h" #include "osr/types.h" #include "osr/ways.h" #include "nigiri/types.h" namespace motis::flex { struct flex_routing_data { osr::sharing_data to_sharing_data() { return {.start_allowed_ = &start_allowed_, .end_allowed_ = &end_allowed_, .through_allowed_ = &through_allowed_, .additional_node_offset_ = additional_node_offset_, .additional_node_coordinates_ = additional_node_coordinates_, .additional_edges_ = additional_edges_}; } nigiri::location_idx_t get_additional_node(osr::node_idx_t const n) const { return additional_nodes_[to_idx(n - additional_node_offset_)]; } osr::bitvec start_allowed_; osr::bitvec through_allowed_; osr::bitvec end_allowed_; osr::node_idx_t::value_t additional_node_offset_; std::vector additional_node_coordinates_; osr::hash_map> additional_edges_; std::vector additional_nodes_; }; } // namespace motis::flex ================================================ FILE: include/motis/flex/mode_id.h ================================================ #pragma once #include "osr/types.h" #include "nigiri/types.h" namespace motis::flex { struct mode_id { mode_id(nigiri::flex_transport_idx_t const t, nigiri::stop_idx_t const stop_idx, osr::direction const dir) : transport_{t}, dir_{dir != osr::direction::kForward}, stop_idx_{stop_idx}, msb_{1U} {} static bool is_flex(nigiri::transport_mode_id_t const x) { return (x & 0x80'00'00'00) == 0x80'00'00'00; } explicit mode_id(nigiri::transport_mode_id_t const x) { std::memcpy(this, &x, sizeof(mode_id)); } osr::direction get_dir() const { return dir_ == 0 ? osr::direction::kForward : osr::direction::kBackward; } nigiri::stop_idx_t get_stop() const { return static_cast(stop_idx_); } nigiri::flex_transport_idx_t get_flex_transport() const { return nigiri::flex_transport_idx_t{transport_}; } nigiri::transport_mode_id_t to_id() const { static_assert(sizeof(mode_id) == sizeof(nigiri::transport_mode_id_t)); auto id = nigiri::transport_mode_id_t{}; std::memcpy(&id, this, sizeof(id)); return id; } nigiri::flex_transport_idx_t::value_t transport_ : 23; nigiri::flex_transport_idx_t::value_t dir_ : 1; nigiri::flex_transport_idx_t::value_t stop_idx_ : 7; nigiri::flex_transport_idx_t::value_t msb_ : 1; }; } // namespace motis::flex ================================================ FILE: include/motis/fwd.h ================================================ #pragma once namespace adr { struct formatter; struct reverse; struct area_database; struct typeahead; struct cache; } // namespace adr namespace osr { struct location; struct sharing_data; struct ways; struct platforms; struct lookup; struct elevation_storage; } // namespace osr namespace nigiri { struct timetable; struct rt_timetable; struct shapes_storage; namespace rt { struct run; struct run_stop; } // namespace rt namespace routing { struct td_offset; struct offset; namespace tb { struct tb_data; } } // namespace routing } // namespace nigiri namespace motis { struct tiles_data; struct rt; struct tag_lookup; struct config; struct railviz_static_index; struct railviz_rt_index; struct elevators; struct metrics_registry; struct way_matches_storage; struct data; struct adr_ext; namespace odm { struct bounds; struct ride_sharing_bounds; } // namespace odm namespace gbfs { struct gbfs_data; struct gbfs_routing_data; } // namespace gbfs namespace flex { struct flex_routing_data; struct flex_areas; } // namespace flex } // namespace motis ================================================ FILE: include/motis/gbfs/compression.h ================================================ #pragma once #include #include #include #include "cista/containers/bitvec.h" #include "utl/verify.h" #include "lz4.h" #include "motis/gbfs/data.h" namespace motis::gbfs { template inline compressed_bitvec compress_bitvec( cista::basic_bitvec const& bv) { auto const* original_data = reinterpret_cast(bv.blocks_.data()); auto const original_bytes = static_cast(bv.blocks_.size() * sizeof(typename cista::basic_bitvec::block_t)); auto const max_compressed_size = LZ4_compressBound(original_bytes); auto cbv = compressed_bitvec{ .data_ = std::unique_ptr{ static_cast( std::malloc(static_cast(max_compressed_size)))}, .original_bytes_ = original_bytes, .bitvec_size_ = bv.size_}; utl::verify(cbv.data_ != nullptr, "could not allocate memory for compressed bitvec"); cbv.compressed_bytes_ = LZ4_compress_default( original_data, cbv.data_.get(), original_bytes, max_compressed_size); utl::verify(cbv.compressed_bytes_ > 0, "could not compress bitvec"); if (auto* compressed = std::realloc( cbv.data_.get(), static_cast(cbv.compressed_bytes_)); compressed != nullptr) { cbv.data_.release(); cbv.data_.reset(static_cast(compressed)); } return cbv; } template inline void decompress_bitvec(compressed_bitvec const& cbv, cista::basic_bitvec& bv) { bv.resize(static_cast::size_type>( cbv.bitvec_size_)); auto const decompressed_bytes = LZ4_decompress_safe( cbv.data_.get(), reinterpret_cast(bv.blocks_.data()), cbv.compressed_bytes_, static_cast( bv.blocks_.size() * sizeof(typename cista::basic_bitvec::block_t))); utl::verify(decompressed_bytes == cbv.original_bytes_, "could not decompress bitvec"); } } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/data.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tg.h" #include "cista/hash.h" #include "cista/strong.h" #include "geo/box.h" #include "geo/latlng.h" #include "utl/helpers/algorithm.h" #include "osr/routing/additional_edge.h" #include "osr/routing/sharing_data.h" #include "osr/types.h" #include "motis/box_rtree.h" #include "motis/config.h" #include "motis/fwd.h" #include "motis/point_rtree.h" #include "motis/types.h" #include "motis/gbfs/lru_cache.h" namespace motis::gbfs { enum class gbfs_version : std::uint8_t { k1 = 0, k2 = 1, k3 = 2, }; using vehicle_type_idx_t = cista::strong; enum class vehicle_form_factor : std::uint8_t { kBicycle = 0, kCargoBicycle = 1, kCar = 2, kMoped = 3, kScooterStanding = 4, kScooterSeated = 5, kOther = 6 }; enum class propulsion_type : std::uint8_t { kHuman = 0, kElectricAssist = 1, kElectric = 2, kCombustion = 3, kCombustionDiesel = 4, kHybrid = 5, kPlugInHybrid = 6, kHydrogenFuelCell = 7 }; enum class return_constraint : std::uint8_t { kFreeFloating = 0, // includes hybrid kAnyStation = 1, kRoundtripStation = 2 }; struct vehicle_type { std::string id_{}; vehicle_type_idx_t idx_{vehicle_type_idx_t::invalid()}; std::string name_{}; vehicle_form_factor form_factor_{}; propulsion_type propulsion_type_{}; return_constraint return_constraint_{}; bool known_return_constraint_{}; // true if taken from feed, false if guessed }; struct temp_vehicle_type { std::string id_; std::string name_; vehicle_form_factor form_factor_{}; propulsion_type propulsion_type_{}; }; enum class vehicle_start_type : std::uint8_t { kStation = 0, kFreeFloating = 1 }; struct system_information { std::string id_; std::string name_; std::string name_short_; std::string operator_; std::string url_; std::string purchase_url_; std::string mail_; std::string color_; }; struct rental_uris { // all fields are optional std::string android_; std::string ios_; std::string web_; }; struct tg_geom_deleter { void operator()(tg_geom* ptr) const { if (ptr != nullptr) { tg_geom_free(ptr); } } }; struct station_information { std::string id_; std::string name_; geo::latlng pos_{}; // optional: std::string address_{}; std::string cross_street_{}; rental_uris rental_uris_{}; std::shared_ptr station_area_{}; geo::box bounding_box() const { if (station_area_) { auto const rect = tg_geom_rect(station_area_.get()); return geo::box{geo::latlng{rect.min.y, rect.min.x}, geo::latlng{rect.max.y, rect.max.x}}; } else { return geo::box{pos_, pos_}; } } }; struct station_status { unsigned num_vehicles_available_{}; hash_map vehicle_types_available_{}; hash_map vehicle_docks_available_{}; bool is_renting_{true}; bool is_returning_{true}; }; struct station { station_information info_{}; station_status status_{}; }; struct vehicle_status { bool operator==(vehicle_status const& o) const { return id_ == o.id_; } auto operator<=>(vehicle_status const& o) const { return id_ <=> o.id_; } std::string id_; geo::latlng pos_; bool is_reserved_{}; bool is_disabled_{}; vehicle_type_idx_t vehicle_type_idx_; std::string station_id_; std::string home_station_id_; rental_uris rental_uris_{}; }; struct rule { bool allows_rental_operation() const { return ride_start_allowed_ || ride_end_allowed_ || ride_through_allowed_ || station_parking_.value_or(false); } std::vector vehicle_type_idxs_{}; bool ride_start_allowed_{}; bool ride_end_allowed_{}; bool ride_through_allowed_{}; std::optional station_parking_{}; }; struct geofencing_restrictions { bool ride_start_allowed_{true}; bool ride_end_allowed_{true}; bool ride_through_allowed_{true}; std::optional station_parking_{}; }; struct zone { zone() = default; zone(tg_geom* geom, std::vector&& rules, std::string&& name) : geom_{geom, tg_geom_deleter{}}, rules_{std::move(rules)}, clockwise_{geom_ && tg_geom_num_polys(geom_.get()) > 0 ? tg_poly_clockwise(tg_geom_poly_at(geom_.get(), 0)) : true}, name_{std::move(name)} {} geo::box bounding_box() const { auto const rect = tg_geom_rect(geom_.get()); return geo::box{geo::latlng{rect.min.y, rect.min.x}, geo::latlng{rect.max.y, rect.max.x}}; } bool allows_rental_operation() const { return utl::any_of( rules_, [](rule const& r) { return r.allows_rental_operation(); }); } std::shared_ptr geom_; std::vector rules_; bool clockwise_{true}; std::string name_; }; struct geofencing_zones { gbfs_version version_{}; std::vector zones_; std::vector global_rules_; void clear(); geofencing_restrictions get_restrictions( geo::latlng const& pos, vehicle_type_idx_t, geofencing_restrictions const& default_restrictions) const; }; struct additional_node { struct station { std::string id_; }; struct vehicle { std::size_t idx_{}; }; std::variant data_; }; struct file_info { bool has_value() const { return expiry_.has_value(); } bool needs_update(std::chrono::system_clock::time_point const now) const { return !expiry_.has_value() || *expiry_ < now; } std::optional expiry_{}; cista::hash_t hash_{}; }; struct provider_file_infos { bool needs_update() const { auto const now = std::chrono::system_clock::now(); return urls_fi_.needs_update(now) || system_information_fi_.needs_update(now) || vehicle_types_fi_.needs_update(now) || station_information_fi_.needs_update(now) || station_status_fi_.needs_update(now) || vehicle_status_fi_.needs_update(now) || geofencing_zones_fi_.needs_update(now); } hash_map urls_{}; file_info urls_fi_{}; file_info system_information_fi_{}; file_info vehicle_types_fi_{}; file_info station_information_fi_{}; file_info station_status_fi_{}; file_info vehicle_status_fi_{}; file_info geofencing_zones_fi_{}; }; struct compressed_bitvec { struct free_deleter { void operator()(char* p) const { std::free(p); } }; std::unique_ptr data_{}; int original_bytes_{}; int compressed_bytes_{}; std::size_t bitvec_size_{}; }; struct routing_data { std::vector additional_nodes_{}; std::vector additional_node_coordinates_; osr::hash_map> additional_edges_{}; osr::bitvec start_allowed_{}; osr::bitvec end_allowed_{}; osr::bitvec through_allowed_{}; bool station_parking_{}; }; struct compressed_routing_data { std::vector additional_nodes_{}; std::vector additional_node_coordinates_; osr::hash_map> additional_edges_{}; compressed_bitvec start_allowed_{}; compressed_bitvec end_allowed_{}; compressed_bitvec through_allowed_{}; }; struct provider_routing_data; struct products_routing_data { products_routing_data(std::shared_ptr&& prd, compressed_routing_data const& compressed); osr::sharing_data get_sharing_data( osr::node_idx_t::value_t const additional_node_offset, bool ignore_return_constraints) const { return {.start_allowed_ = &start_allowed_, .end_allowed_ = ignore_return_constraints ? nullptr : &end_allowed_, .through_allowed_ = &through_allowed_, .additional_node_offset_ = additional_node_offset, .additional_node_coordinates_ = compressed_.additional_node_coordinates_, .additional_edges_ = compressed_.additional_edges_}; } std::shared_ptr provider_routing_data_; compressed_routing_data const& compressed_; osr::bitvec start_allowed_; osr::bitvec end_allowed_; osr::bitvec through_allowed_; }; using gbfs_products_idx_t = cista::strong; struct provider_routing_data : std::enable_shared_from_this { std::shared_ptr get_products_routing_data( gbfs_products_idx_t const prod_idx) const { return std::make_shared( shared_from_this(), products_.at(to_idx(prod_idx))); } std::vector products_; }; struct provider_products { bool includes_vehicle_type(vehicle_type_idx_t const idx) const { return (idx == vehicle_type_idx_t::invalid() && vehicle_types_.empty()) || utl::find(vehicle_types_, idx) != end(vehicle_types_); } gbfs_products_idx_t idx_{gbfs_products_idx_t::invalid()}; std::vector vehicle_types_; vehicle_form_factor form_factor_{vehicle_form_factor::kBicycle}; propulsion_type propulsion_type_{propulsion_type::kHuman}; return_constraint return_constraint_{}; bool known_return_constraint_{}; // true if taken from feed, false if guessed bool has_vehicles_to_rent_{}; }; struct gbfs_products_ref { friend bool operator==(gbfs_products_ref const&, gbfs_products_ref const&) = default; explicit operator bool() const noexcept { return provider_ != gbfs_provider_idx_t::invalid(); } gbfs_provider_idx_t provider_{gbfs_provider_idx_t::invalid()}; gbfs_products_idx_t products_{gbfs_products_idx_t::invalid()}; }; struct gbfs_provider { std::string id_; // from config gbfs_provider_idx_t idx_{gbfs_provider_idx_t::invalid()}; std::string group_id_; std::shared_ptr file_infos_{}; system_information sys_info_{}; std::map stations_{}; vector_map vehicle_types_{}; hash_map, vehicle_type_idx_t> vehicle_types_map_{}; hash_map temp_vehicle_types_{}; std::vector vehicle_status_; geofencing_zones geofencing_zones_{}; geofencing_restrictions default_restrictions_{}; std::optional default_return_constraint_{}; vector_map products_; bool has_vehicles_to_rent_{}; geo::box bbox_{}; std::optional color_{}; }; struct gbfs_group { std::string id_; std::string name_; std::optional color_{}; std::vector providers_{}; }; struct oauth_state { config::gbfs::oauth_settings settings_; std::string access_token_{}; std::optional expiry_{}; unsigned expires_in_{}; }; struct provider_feed { bool operator==(provider_feed const& o) const { return id_ == o.id_; } bool operator==(std::string const& id) const { return id_ == id; } bool operator<(provider_feed const& o) const { return id_ < o.id_; } std::string id_; std::string url_; headers_t headers_{}; std::optional dir_{}; geofencing_restrictions default_restrictions_{}; std::optional default_return_constraint_{}; std::optional config_group_{}; std::optional config_color_{}; std::shared_ptr oauth_{}; std::map default_ttl_{}; std::map overwrite_ttl_{}; }; struct aggregated_feed { bool operator==(aggregated_feed const& o) const { return id_ == o.id_; } bool operator==(std::string const& id) const { return id_ == id; } bool operator<(aggregated_feed const& o) const { return id_ < o.id_; } bool needs_update() const { return !expiry_.has_value() || expiry_.value() < std::chrono::system_clock::now(); } std::string id_; std::string url_; headers_t headers_{}; std::optional expiry_{}; std::vector feeds_{}; std::shared_ptr oauth_{}; std::map default_ttl_{}; std::map overwrite_ttl_{}; }; struct gbfs_data { explicit gbfs_data(std::size_t const cache_size) : cache_{cache_size} {} std::shared_ptr get_products_routing_data( osr::ways const& w, osr::lookup const& l, gbfs_products_ref); std::shared_ptr>> standalone_feeds_{}; std::shared_ptr>> aggregated_feeds_{}; vector_map> providers_{}; hash_map provider_by_id_{}; point_rtree provider_rtree_{}; box_rtree provider_zone_rtree_{}; hash_map groups_{}; lru_cache cache_; // used to share decompressed routing data between routing requests std::mutex products_routing_data_mutex_; hash_map> products_routing_data_{}; }; } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/gbfs_output.h ================================================ #pragma once #include "motis/osr/street_routing.h" namespace motis::gbfs { struct gbfs_output final : public output { gbfs_output(osr::ways const&, gbfs::gbfs_routing_data&, gbfs::gbfs_products_ref, bool ignore_rental_return_constraints); ~gbfs_output() override; api::ModeEnum get_mode() const override; osr::search_profile get_profile() const override; bool is_time_dependent() const override; transport_mode_t get_cache_key() const override; osr::sharing_data const* get_sharing_data() const override; void annotate_leg(nigiri::lang_t const&, osr::node_idx_t const from_node, osr::node_idx_t const to_node, api::Leg&) const override; api::Place get_place(nigiri::lang_t const&, osr::node_idx_t, std::optional const& tz) const override; std::size_t get_additional_node_idx(osr::node_idx_t const n) const; private: std::string get_node_name(osr::node_idx_t) const; osr::ways const& w_; gbfs::gbfs_routing_data& gbfs_rd_; gbfs::gbfs_provider const& provider_; gbfs::provider_products const& products_; gbfs::products_routing_data const* prod_rd_; osr::sharing_data sharing_data_; api::Rental rental_; }; } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/geofencing.h ================================================ #pragma once #include #include #include "tg.h" #include "geo/latlng.h" #include "motis/gbfs/data.h" namespace motis::gbfs { bool applies(std::vector const& rule_vehicle_type_idxs, std::vector const& segment_vehicle_type_idxs); bool multipoly_contains_point(tg_geom const* geom, geo::latlng const& pos); } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/lru_cache.h ================================================ #pragma once #include #include #include #include #include #include "motis/types.h" #include "utl/helpers/algorithm.h" namespace motis::gbfs { template class lru_cache { public: explicit lru_cache(std::size_t const max_size) : max_size_{max_size} {} lru_cache(lru_cache const& o) : max_size_{o.max_size_} { auto read_lock = std::shared_lock{o.mutex_}; cache_map_ = o.cache_map_; lru_order_ = o.lru_order_; pending_computations_.clear(); } lru_cache& operator=(lru_cache const& o) { if (this != &o) { auto read_lock = std::shared_lock{o.mutex_}; auto write_lock = std::unique_lock{mutex_}; max_size_ = o.max_size_; cache_map_ = o.cache_map_; lru_order_ = o.lru_order_; pending_computations_.clear(); } return *this; } template std::shared_ptr get_or_compute(Key const key, F compute_fn) { // check with shared lock if entry already exists { auto read_lock = std::shared_lock{mutex_}; if (auto it = cache_map_.find(key); it != cache_map_.end()) { move_to_front(key); return it->second->value_; } } // not found -> acquire exclusive lock to modify the cache auto write_lock = std::unique_lock{mutex_}; // check again in case another thread inserted it if (auto it = cache_map_.find(key); it != cache_map_.end()) { move_to_front(key); return it->second->value_; } // if another thread is already computing it, wait for it if (auto it = pending_computations_.find(key); it != pending_computations_.end()) { auto future = it->second; write_lock.unlock(); return future.get(); } // create pending computation auto promise = std::promise>{}; auto shared_future = promise.get_future().share(); pending_computations_[key] = shared_future; write_lock.unlock(); // compute the value auto value = compute_fn(); // store the result write_lock.lock(); if (lru_order_.size() >= max_size_) { // evict least recently used cache entry auto const last_key = lru_order_.back(); cache_map_.erase(last_key); lru_order_.pop_back(); } cache_map_.try_emplace( key, std::make_shared(cache_entry{key, value})); lru_order_.insert(lru_order_.begin(), key); pending_computations_.erase(key); promise.set_value(value); return value; } std::shared_ptr get(Key const key) { auto read_lock = std::shared_lock{mutex_}; if (auto it = cache_map_.find(key); it != cache_map_.end()) { return it->second->value_; } return nullptr; } bool contains(Key const key) { auto read_lock = std::shared_lock{mutex_}; return cache_map_.find(key) != cache_map_.end(); } template void update_if_exists(Key const key, F update_fn) { auto write_lock = std::unique_lock{mutex_}; if (auto it = cache_map_.find(key); it != cache_map_.end()) { it->second->value_ = update_fn(it->second->value_); move_to_front(key); } } /// adds an entry to the cache if there is still space or updates /// an existing entry if it already exists template bool try_add_or_update(Key const key, F compute_fn) { auto write_lock = std::unique_lock{mutex_}; if (auto it = cache_map_.find(key); it != cache_map_.end()) { it->second->value_ = compute_fn(); move_to_front(key); return true; } if (lru_order_.size() >= max_size_) { return false; } cache_map_.try_emplace( key, std::make_shared(cache_entry{key, compute_fn()})); lru_order_.insert(lru_order_.begin(), key); return true; } void remove(Key const key) { auto write_lock = std::unique_lock{mutex_}; if (auto it = cache_map_.find(key); it != cache_map_.end()) { if (auto const lru_it = utl::find(lru_order_, key); lru_it != lru_order_.end()) { lru_order_.erase(lru_it); } cache_map_.erase(it); } } std::vector>> get_all_entries() const { auto read_lock = std::shared_lock{mutex_}; auto entries = std::vector>>{}; entries.reserve(lru_order_.size()); for (auto const it = lru_order_.rbegin(); it != lru_order_.rend(); ++it) { if (auto const map_it = cache_map_.find(*it); map_it != cache_map_.end()) { entries.emplace_back(map_it->first, map_it->second->value_); } } return entries; } std::size_t size() const { return lru_order_.size(); } bool empty() const { return lru_order_.empty(); } private: struct cache_entry { Key key_{}; std::shared_ptr value_{}; }; void move_to_front(Key const key) { auto const it = utl::find(lru_order_, key); if (it != lru_order_.end()) { lru_order_.erase(it); lru_order_.insert(lru_order_.begin(), key); } } std::size_t max_size_; hash_map> cache_map_; std::vector lru_order_; hash_map>> pending_computations_{}; mutable std::shared_mutex mutex_; }; } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/mode.h ================================================ #pragma once #include #include #include "motis/gbfs/data.h" #include "motis-api/motis-api.h" namespace motis::gbfs { api::RentalFormFactorEnum to_api_form_factor(vehicle_form_factor); vehicle_form_factor from_api_form_factor(api::RentalFormFactorEnum); api::RentalPropulsionTypeEnum to_api_propulsion_type(propulsion_type); propulsion_type from_api_propulsion_type(api::RentalPropulsionTypeEnum); api::RentalReturnConstraintEnum to_api_return_constraint(return_constraint); bool products_match( provider_products const& prod, std::optional> const& form_factors, std::optional> const& propulsion_types); } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/osr_mapping.h ================================================ #pragma once #include "motis/fwd.h" namespace motis::gbfs { struct gbfs_provider; struct provider_routing_data; void map_data(osr::ways const&, osr::lookup const&, gbfs_provider const&, provider_routing_data&); } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/osr_profile.h ================================================ #pragma once #include "osr/routing/profile.h" #include "motis/gbfs/data.h" namespace motis::gbfs { osr::search_profile get_osr_profile(vehicle_form_factor const&); } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/parser.h ================================================ #pragma once #include #include "boost/json.hpp" #include "motis/gbfs/data.h" #include "motis/types.h" namespace motis::gbfs { hash_map parse_discovery( boost::json::value const& root); std::optional parse_return_constraint(std::string_view s); void load_system_information(gbfs_provider&, boost::json::value const& root); void load_station_information(gbfs_provider&, boost::json::value const& root); void load_station_status(gbfs_provider&, boost::json::value const& root); void load_vehicle_types(gbfs_provider&, boost::json::value const& root); void load_vehicle_status(gbfs_provider&, boost::json::value const& root); void load_geofencing_zones(gbfs_provider&, boost::json::value const& root); } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/partition.h ================================================ #pragma once #include #include #include #include #include "cista/strong.h" namespace motis::gbfs { template struct partition { explicit partition(T const n) : n_{n} { partition_.resize(static_cast(cista::to_idx(n))); for (auto i = T{0}; i < n; ++i) { partition_[static_cast(cista::to_idx(i))] = i; } if (n != 0) { // initially there's only one set ending at n-1 set_ends_.push_back(n - 1); } } void refine(std::span const s) { if (s.empty()) { return; } // mark elements in s auto in_s = std::vector(static_cast(cista::to_idx(n_)), false); for (auto const elem : s) { assert(elem < n_); in_s[static_cast(cista::to_idx(elem))] = true; } // process each existing set auto current_start = T{0}; auto new_set_ends = std::vector{}; new_set_ends.reserve(2 * set_ends_.size()); for (auto const set_end : set_ends_) { // count elements in current set that are in s auto count = T{0}; for (auto i = current_start; i <= set_end; ++i) { if (in_s[static_cast(cista::to_idx( partition_[static_cast(cista::to_idx(i))]))]) { ++count; } } auto const set_size = set_end - current_start + 1; // if split is needed (some but not all elements are in s) if (count != 0 && count != set_size) { // partition the set into two parts auto split_pos = current_start; for (auto i = current_start; i <= set_end; ++i) { if (in_s[static_cast(cista::to_idx( partition_[static_cast(cista::to_idx(i))]))]) { // move element to front of split if (i != split_pos) { std::swap(partition_[static_cast(cista::to_idx(i))], partition_[static_cast( cista::to_idx(split_pos))]); } ++split_pos; } } // add end positions for both new sets new_set_ends.push_back(split_pos - 1); new_set_ends.push_back(set_end); } else { // no split needed, keep original set new_set_ends.push_back(set_end); } current_start = set_end + 1; } set_ends_ = std::move(new_set_ends); } std::vector> get_sets() const { auto result = std::vector>{}; result.reserve(set_ends_.size()); auto current_start = T{0}; for (auto const set_end : set_ends_) { auto set = std::vector{}; set.reserve( static_cast(cista::to_idx(set_end - current_start + 1))); for (auto i = current_start; i <= set_end; ++i) { set.push_back(partition_[static_cast(cista::to_idx(i))]); } result.push_back(std::move(set)); current_start = set_end + 1; } return result; } // the number of elements in the partition - the original set // contains the elements 0, 1, ..., n - 1 T n_; // stores the elements grouped by sets - the elements of each set are // stored contiguously, e.g. "0345789" could be {{0, 3, 4}, {5}, {7, 8, 9}} // or {{0}, {3, 4}, {5}, {7, 8, 9}}, depending on set_ends_. std::vector partition_; // stores the end index of each set in partition_ std::vector set_ends_; }; } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/routing_data.h ================================================ #pragma once #include #include #include "motis/fwd.h" #include "motis/gbfs/data.h" #include "motis/types.h" namespace motis::gbfs { struct gbfs_routing_data { gbfs_routing_data() = default; gbfs_routing_data(osr::ways const* w, osr::lookup const* l, std::shared_ptr data) : w_{w}, l_{l}, data_{std::move(data)} {} bool has_data() const { return data_ != nullptr; } std::shared_ptr get_provider_routing_data( gbfs_provider const&); products_routing_data* get_products_routing_data( gbfs_provider const& provider, gbfs_products_idx_t prod_idx); products_routing_data* get_products_routing_data(gbfs_products_ref); provider_products const& get_products(gbfs_products_ref); nigiri::transport_mode_id_t get_transport_mode(gbfs_products_ref); gbfs_products_ref get_products_ref(nigiri::transport_mode_id_t) const; osr::ways const* w_{}; osr::lookup const* l_{}; std::shared_ptr data_{}; hash_map> products_; std::vector products_refs_; hash_map products_ref_to_transport_mode_; }; std::shared_ptr compute_provider_routing_data( osr::ways const&, osr::lookup const&, gbfs_provider const&); std::shared_ptr get_provider_routing_data( osr::ways const&, osr::lookup const&, gbfs_data&, gbfs_provider const&); } // namespace motis::gbfs ================================================ FILE: include/motis/gbfs/update.h ================================================ #pragma once #include #include "boost/asio/awaitable.hpp" #include "boost/asio/io_context.hpp" #include "motis/fwd.h" namespace motis::gbfs { boost::asio::awaitable update(config const&, osr::ways const&, osr::lookup const&, std::shared_ptr&); void run_gbfs_update(boost::asio::io_context&, config const&, osr::ways const&, osr::lookup const&, std::shared_ptr&); } // namespace motis::gbfs ================================================ FILE: include/motis/get_loc.h ================================================ #pragma once #include "osr/platforms.h" #include "osr/routing/route.h" #include "nigiri/timetable.h" #include "motis/constants.h" #include "motis/match_platforms.h" #include "motis/types.h" namespace motis { inline osr::location get_loc( nigiri::timetable const& tt, osr::ways const& w, osr::platforms const& pl, vector_map const& matches, nigiri::location_idx_t const l) { auto pos = tt.locations_.coordinates_[l]; if (matches[l] != osr::platform_idx_t::invalid()) { auto const center = get_platform_center(pl, w, matches[l]); if (center.has_value() && geo::distance(*center, pos) < kMaxAdjust) { pos = *center; } } auto const lvl = matches[l] == osr::platform_idx_t::invalid() ? osr::level_t{0.F} : pl.get_level(w, matches[l]); return {pos, lvl}; } } // namespace motis ================================================ FILE: include/motis/get_stops_with_traffic.h ================================================ #pragma once #include "nigiri/types.h" #include "motis/fwd.h" #include "motis/point_rtree.h" namespace motis { std::vector get_stops_with_traffic( nigiri::timetable const&, nigiri::rt_timetable const*, point_rtree const&, osr::location const&, double const distance, nigiri::location_idx_t const not_equal_to = nigiri::location_idx_t::invalid()); } // namespace motis ================================================ FILE: include/motis/hashes.h ================================================ #pragma once #include #include #include #include namespace motis { using meta_entry_t = std::pair; using meta_t = std::map; constexpr auto const osr_version = []() { return meta_entry_t{"osr_bin_ver", 34U}; }; constexpr auto const adr_version = []() { return meta_entry_t{"adr_bin_ver", 14U}; }; constexpr auto const adr_ext_version = []() { return meta_entry_t{"adr_ext_bin_ver", 4U}; }; constexpr auto const n_version = []() { return meta_entry_t{"nigiri_bin_ver", 33U}; }; constexpr auto const tbd_version = []() { return meta_entry_t{"tbd_bin_ver", 1U}; }; constexpr auto const matches_version = []() { return meta_entry_t{"matches_bin_ver", 5U}; }; constexpr auto const tiles_version = []() { return meta_entry_t{"tiles_bin_ver", 1U}; }; constexpr auto const osr_footpath_version = []() { return meta_entry_t{"osr_footpath_bin_ver", 3U}; }; constexpr auto const routed_shapes_version = []() { return meta_entry_t{"routed_shapes_ver", 10U}; }; std::string to_str(meta_t const&); meta_t read_hashes(std::filesystem::path const& data_path, std::string const& name); void write_hashes(std::filesystem::path const& data_path, std::string const& name, meta_t const& h); } // namespace motis ================================================ FILE: include/motis/http_req.h ================================================ #pragma once #include #include #include #include "boost/asio/awaitable.hpp" #include "boost/beast/http/dynamic_body.hpp" #include "boost/beast/http/message.hpp" #include "boost/url/url.hpp" namespace motis { constexpr auto const kBodySizeLimit = 512U * 1024U * 1024U; // 512 M using http_response = boost::beast::http::response; struct proxy { bool use_tls_; std::string host_, port_; }; boost::asio::awaitable http_GET( boost::urls::url, std::map const& headers, std::chrono::seconds timeout, std::optional const& = std::nullopt); boost::asio::awaitable http_POST( boost::urls::url, std::map const& headers, std::string const& body, std::chrono::seconds timeout, std::optional const& = std::nullopt); std::string get_http_body(http_response const&); } // namespace motis ================================================ FILE: include/motis/import.h ================================================ #pragma once #include #include "motis/config.h" namespace motis { void import(config const&, std::filesystem::path const& data_path, std::optional> const& task_filter = {}); } // namespace motis ================================================ FILE: include/motis/journey_to_response.h ================================================ #pragma once #include #include #include "nigiri/routing/journey.h" #include "nigiri/rt/frun.h" #include "nigiri/types.h" #include "osr/location.h" #include "osr/routing/route.h" #include "osr/types.h" #include "motis-api/motis-api.h" #include "motis/elevators/elevators.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" #include "motis/osr/street_routing.h" #include "motis/place.h" #include "motis/types.h" namespace motis { double get_level(osr::ways const*, osr::platforms const*, platform_matches_t const*, nigiri::location_idx_t); std::optional> get_alerts( nigiri::rt::frun const&, std::optional> const&, bool fuzzy_stop, std::optional> const& language); api::Itinerary journey_to_response( osr::ways const*, osr::lookup const*, osr::platforms const*, nigiri::timetable const&, tag_lookup const&, flex::flex_areas const*, elevators const* e, nigiri::rt_timetable const*, platform_matches_t const* matches, osr::elevation_storage const*, nigiri::shapes_storage const*, gbfs::gbfs_routing_data&, adr_ext const*, tz_map_t const*, nigiri::routing::journey const&, place_t const& start, place_t const& dest, street_routing_cache_t&, osr::bitvec* blocked_mem, bool car_transfers, osr_parameters const&, api::PedestrianProfileEnum, api::ElevationCostsEnum, bool join_interlined_legs, bool detailed_transfers, bool detailed_legs, bool with_fares, bool with_scheduled_skipped_stops, double timetable_max_matching_distance, double max_matching_distance, unsigned api_version, bool ignore_start_rental_return_constraints, bool ignore_dest_rental_return_constraints, std::optional> const& language); } // namespace motis ================================================ FILE: include/motis/location_routes.h ================================================ #pragma once #include "nigiri/timetable.h" namespace motis { inline nigiri::hash_set get_location_routes( nigiri::timetable const& tt, nigiri::location_idx_t const l) { auto names = nigiri::hash_set{}; for (auto const r : tt.location_routes_[l]) { for (auto const t : tt.route_transport_ranges_[r]) { names.emplace(tt.transport_name(t)); } } return names; } } // namespace motis ================================================ FILE: include/motis/logging.h ================================================ #pragma once #include #include "motis/config.h" namespace motis { int set_log_level(config const&); int set_log_level(std::string&& log_lvl); } // namespace motis ================================================ FILE: include/motis/match_platforms.h ================================================ #pragma once #include #include #include "osr/lookup.h" #include "osr/types.h" #include "nigiri/types.h" #include "motis/data.h" #include "motis/types.h" namespace motis { using platform_matches_t = vector_map; struct way_matches_storage { way_matches_storage(std::filesystem::path, cista::mmap::protection, double max_matching_distance); cista::mmap mm(char const* file); cista::mmap::protection mode_; std::filesystem::path p_; osr::mm_vecvec matches_; double max_matching_distance_; void preprocess_osr_matches(nigiri::timetable const&, osr::platforms const&, osr::ways const&, osr::lookup const&, platform_matches_t const&); }; std::optional get_platform_center(osr::platforms const&, osr::ways const&, osr::platform_idx_t); osr::platform_idx_t get_match(nigiri::timetable const&, osr::platforms const&, osr::ways const&, nigiri::location_idx_t); platform_matches_t get_matches(nigiri::timetable const&, osr::platforms const&, osr::ways const&); std::optional get_track(std::string_view); std::vector get_reverse_platform_way_matches( osr::lookup const&, way_matches_storage const*, osr::search_profile, std::span, std::span, osr::direction, double max_matching_distance); } // namespace motis ================================================ FILE: include/motis/metrics_registry.h ================================================ #pragma once #include "prometheus/counter.h" #include "prometheus/family.h" #include "prometheus/gauge.h" #include "prometheus/histogram.h" #include "prometheus/registry.h" namespace motis { struct metrics_registry { metrics_registry(); ~metrics_registry(); prometheus::Registry registry_; prometheus::Counter& routing_requests_; prometheus::Counter& one_to_many_requests_; prometheus::Counter& routing_journeys_found_; prometheus::Family& routing_odm_journeys_found_; prometheus::Histogram& routing_odm_journeys_found_blacklist_; prometheus::Histogram& routing_odm_journeys_found_whitelist_; prometheus::Histogram& routing_odm_journeys_found_non_dominated_pareto_; prometheus::Histogram& routing_odm_journeys_found_non_dominated_cost_; prometheus::Histogram& routing_odm_journeys_found_non_dominated_prod_; prometheus::Histogram& routing_odm_journeys_found_non_dominated_; prometheus::Histogram& routing_journey_duration_seconds_; prometheus::Family& routing_execution_duration_seconds_; prometheus::Histogram& routing_execution_duration_seconds_init_; prometheus::Histogram& routing_execution_duration_seconds_blacklisting_; prometheus::Histogram& routing_execution_duration_seconds_preparing_; prometheus::Histogram& routing_execution_duration_seconds_routing_; prometheus::Histogram& routing_execution_duration_seconds_whitelisting_; prometheus::Histogram& routing_execution_duration_seconds_mixing_; prometheus::Histogram& routing_execution_duration_seconds_total_; prometheus::Family& current_trips_running_scheduled_count_; prometheus::Family& current_trips_running_scheduled_with_realtime_count_; prometheus::Gauge& total_trips_with_realtime_count_; prometheus::Family& timetable_first_day_timestamp_; prometheus::Family& timetable_last_day_timestamp_; prometheus::Family& timetable_locations_count_; prometheus::Family& timetable_trips_count_; prometheus::Family& timetable_transports_x_days_count_; private: metrics_registry(prometheus::Histogram::BucketBoundaries event_boundaries, prometheus::Histogram::BucketBoundaries time_boundaries); }; } // namespace motis ================================================ FILE: include/motis/motis_instance.h ================================================ #include #include #include "boost/asio/io_context.hpp" #include "net/web_server/query_router.h" #include "utl/set_thread_name.h" #include "motis/endpoints/adr/geocode.h" #include "motis/endpoints/adr/reverse_geocode.h" #include "motis/endpoints/elevators.h" #include "motis/endpoints/graph.h" #include "motis/endpoints/gtfsrt.h" #include "motis/endpoints/initial.h" #include "motis/endpoints/levels.h" #include "motis/endpoints/map/flex_locations.h" #include "motis/endpoints/map/rental.h" #include "motis/endpoints/map/route_details.h" #include "motis/endpoints/map/routes.h" #include "motis/endpoints/map/shapes_debug.h" #include "motis/endpoints/map/stops.h" #include "motis/endpoints/map/trips.h" #include "motis/endpoints/matches.h" #include "motis/endpoints/metrics.h" #include "motis/endpoints/ojp.h" #include "motis/endpoints/one_to_all.h" #include "motis/endpoints/one_to_many.h" #include "motis/endpoints/one_to_many_post.h" #include "motis/endpoints/osr_routing.h" #include "motis/endpoints/platforms.h" #include "motis/endpoints/routing.h" #include "motis/endpoints/stop_times.h" #include "motis/endpoints/tiles.h" #include "motis/endpoints/transfers.h" #include "motis/endpoints/trip.h" #include "motis/endpoints/update_elevator.h" #include "motis/gbfs/update.h" #include "motis/metrics_registry.h" #include "motis/rt_update.h" namespace motis { struct io_thread { template io_thread(char const* name, Fn&& f) { ioc_ = std::make_unique(); t_ = std::make_unique( [ioc = ioc_.get(), name, f = std::move(f)]() { utl::set_current_thread_name(name); f(*ioc); ioc->run(); }); } io_thread() = default; void stop() { if (ioc_ == nullptr) { return; } ioc_->stop(); } void join() { if (t_ == nullptr) { return; } t_->join(); } std::unique_ptr t_; std::unique_ptr ioc_; }; template struct motis_instance { motis_instance(Executor&& exec, data& d, config const& c, std::string_view motis_version) : qr_{std::forward(exec)} { qr_.add_header("Server", fmt::format("MOTIS {}", motis_version)); d.motis_version_ = motis_version; if (c.server_.value_or(config::server{}).data_attribution_link_) { qr_.add_header("Link", fmt::format("<{}>; rel=\"license\"", *c.server_->data_attribution_link_)); } POST("/api/matches", d); POST("/api/elevators", d); POST("/api/route", d); POST("/api/platforms", d); POST("/api/graph", d); GET("/api/debug/transfers", d); GET("/api/debug/flex", d); GET("/api/v1/map/levels", d); GET("/api/v1/map/initial", d); GET("/api/v1/reverse-geocode", d); GET("/api/v1/geocode", d); GET("/api/v1/plan", d); GET("/api/v2/plan", d); GET("/api/v3/plan", d); GET("/api/v4/plan", d); GET("/api/v5/plan", d); GET("/api/v1/stoptimes", d); GET("/api/v4/stoptimes", d); GET("/api/v5/stoptimes", d); GET("/api/v1/trip", d); GET("/api/v2/trip", d); GET("/api/v4/trip", d); GET("/api/v5/trip", d); GET("/api/v1/map/trips", d); GET("/api/v4/map/trips", d); GET("/api/v5/map/trips", d); GET("/api/v1/map/stops", d); GET("/api/experimental/map/route-details", d); GET("/api/experimental/map/routes", d); GET("/api/v1/map/rentals", d); GET("/api/v1/rentals", d); GET("/api/experimental/one-to-all", d); GET("/api/v1/one-to-all", d); GET("/api/v1/one-to-many", d); GET("/api/experimental/one-to-many-intermodal", d); POST( "/api/experimental/one-to-many-intermodal", d); POST("/api/v1/one-to-many", d); if (!c.requires_rt_timetable_updates()) { // Elevator updates are not compatible with RT-updates. POST("/api/update_elevator", d); } if (c.shapes_debug_api_enabled()) { utl::verify(d.w_ != nullptr && d.l_ != nullptr && d.tt_ != nullptr && d.tags_ != nullptr, "data for shapes debug api not loaded"); qr_.route("GET", "/api/experimental/shapes-debug/", ep::shapes_debug{c, d.w_.get(), d.l_.get(), d.tt_.get(), d.tags_.get()}); } if (c.tiles_) { utl::verify(d.tiles_ != nullptr, "tiles data not loaded"); qr_.route("GET", "/tiles/", ep::tiles{*d.tiles_}); } qr_.route("POST", "/ojp20", ep::ojp{ .routing_ep_ = utl::init_from(d), .geocoding_ep_ = utl::init_from(d), .stops_ep_ = utl::init_from(d), .stop_times_ep_ = utl::init_from(d), .trip_ep_ = utl::init_from(d), }); qr_.route("GET", "/metrics", ep::metrics{d.tt_.get(), d.tags_.get(), d.rt_, d.metrics_.get()}); qr_.route("GET", "/gtfsrt", ep::gtfsrt{c, d.tt_.get(), d.tags_.get(), d.rt_}); qr_.serve_files(c.server_.value_or(config::server{}).web_folder_); qr_.enable_cors(); } template void GET(std::string target, From& from) { if (auto x = utl::init_from(from); x.has_value()) { qr_.get(std::move(target), std::move(*x)); } } template void POST(std::string target, From& from) { if (auto x = utl::init_from(from); x.has_value()) { qr_.post(std::move(target), std::move(*x)); } } void run(data& d, config const& c) { if (d.w_ && d.l_ && c.has_gbfs_feeds()) { gbfs_ = io_thread{"motis gbfs update", [&](boost::asio::io_context& ioc) { gbfs::run_gbfs_update(ioc, c, *d.w_, *d.l_, d.gbfs_); }}; } if (c.requires_rt_timetable_updates()) { rt_ = io_thread{"motis rt update", [&](boost::asio::io_context& ioc) { run_rt_update(ioc, c, d); }}; } } void stop() { rt_.stop(); gbfs_.stop(); } void join() { rt_.join(); gbfs_.join(); } net::query_router qr_{}; io_thread rt_, gbfs_; }; } // namespace motis ================================================ FILE: include/motis/odm/bounds.h ================================================ #pragma once #include #include "geo/latlng.h" struct tg_geom; namespace motis::odm { struct bounds { explicit bounds(std::filesystem::path const&); ~bounds(); bool contains(geo::latlng const&) const; tg_geom* geom_{nullptr}; }; struct ride_sharing_bounds : bounds { using bounds::bounds; }; } // namespace motis::odm ================================================ FILE: include/motis/odm/journeys.h ================================================ #pragma once #include "nigiri/routing/journey.h" #include "nigiri/routing/pareto_set.h" namespace motis::odm { std::vector from_csv(std::string_view); nigiri::pareto_set separate_pt( std::vector&); std::string to_csv(nigiri::routing::journey const&); std::string to_csv(std::vector const&); nigiri::routing::journey make_odm_direct(nigiri::location_idx_t from, nigiri::location_idx_t to, nigiri::unixtime_t departure, nigiri::unixtime_t arrival); } // namespace motis::odm ================================================ FILE: include/motis/odm/meta_router.h ================================================ #pragma once #include #include #include #include "osr/location.h" #include "nigiri/types.h" #include "motis-api/motis-api.h" #include "motis/endpoints/routing.h" #include "motis/fwd.h" #include "motis/gbfs/routing_data.h" #include "motis/odm/query_factory.h" #include "motis/place.h" namespace nigiri { struct timetable; struct rt_timetable; } // namespace nigiri namespace nigiri::routing { struct query; struct journey; } // namespace nigiri::routing namespace motis::odm { struct meta_router { meta_router(ep::routing const&, api::plan_params const&, std::vector const& pre_transit_modes, std::vector const& post_transit_modes, std::vector const& direct_modes, std::variant const& from, std::variant const& to, api::Place const& from_p, api::Place const& to_p, nigiri::routing::query const& start_time, std::vector& direct, nigiri::duration_t fastest_direct_, bool odm_pre_transit, bool odm_post_transit, bool odm_direct, bool ride_sharing_pre_transit, bool ride_sharing_post_transit, bool ride_sharing_direct, unsigned api_version); ~meta_router(); api::plan_response run(); struct routing_result { routing_result() = default; routing_result(nigiri::routing::routing_result rr) : journeys_{*rr.journeys_}, interval_{rr.interval_}, search_stats_{rr.search_stats_}, algo_stats_{std::move(rr.algo_stats_)} {} nigiri::pareto_set journeys_{}; nigiri::interval interval_{}; nigiri::routing::search_stats search_stats_{}; std::map algo_stats_{}; }; private: nigiri::routing::query get_base_query( nigiri::interval const&) const; std::vector search_interval( std::vector const&) const; ep::routing const& r_; api::plan_params const& query_; std::vector const& pre_transit_modes_; std::vector const& post_transit_modes_; std::vector const& direct_modes_; std::variant const& from_; std::variant const& to_; api::Place const& from_place_; api::Place const& to_place_; nigiri::routing::query const& start_time_; std::vector& direct_; nigiri::duration_t fastest_direct_; bool odm_pre_transit_; bool odm_post_transit_; bool odm_direct_; bool ride_sharing_pre_transit_; bool ride_sharing_post_transit_; bool ride_sharing_direct_; unsigned api_version_; nigiri::timetable const* tt_; std::shared_ptr const rt_; nigiri::rt_timetable const* rtt_; motis::elevators const* e_; gbfs::gbfs_routing_data gbfs_rd_; std::variant const& start_; std::variant const& dest_; std::vector start_modes_; std::vector dest_modes_; std::optional> const& start_form_factors_; std::optional> const& dest_form_factors_; std::optional> const& start_propulsion_types_; std::optional> const& dest_propulsion_types_; std::optional> const& start_rental_providers_; std::optional> const& dest_rental_providers_; std::optional> const& start_rental_provider_groups_; std::optional> const& dest_rental_provider_groups_; bool start_ignore_rental_return_constraints_{}; bool dest_ignore_rental_return_constraints_{}; }; } // namespace motis::odm ================================================ FILE: include/motis/odm/odm.h ================================================ #pragma once #include "nigiri/routing/journey.h" #include "nigiri/routing/start_times.h" #include "nigiri/types.h" #include "motis-api/motis-api.h" namespace motis::odm { constexpr auto const kODMTransferBuffer = nigiri::duration_t{5}; constexpr auto const kWalkTransportModeId = static_cast(api::ModeEnum::WALK); bool by_stop(nigiri::routing::start const&, nigiri::routing::start const&); enum which_mile { kFirstMile, kLastMile }; bool is_odm_leg(nigiri::routing::journey::leg const&, nigiri::transport_mode_id_t); bool uses_odm(nigiri::routing::journey const&, nigiri::transport_mode_id_t); bool is_pure_pt(nigiri::routing::journey const&); bool is_direct_odm(nigiri::routing::journey const&); nigiri::duration_t odm_time(nigiri::routing::journey::leg const&); nigiri::duration_t odm_time(nigiri::routing::journey const&); nigiri::duration_t pt_time(nigiri::routing::journey const&); nigiri::duration_t duration(nigiri::routing::start const&); std::string odm_label(nigiri::routing::journey const&); } // namespace motis::odm ================================================ FILE: include/motis/odm/prima.h ================================================ #pragma once #include #include #include "geo/latlng.h" #include "nigiri/routing/journey.h" #include "nigiri/routing/start_times.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/place.h" namespace motis::ep { struct routing; } // namespace motis::ep namespace motis::odm { constexpr auto kODMDirectPeriod = std::chrono::seconds{300}; constexpr auto kODMDirectFactor = 1.0; constexpr auto kODMOffsetMinImprovement = std::chrono::seconds{60}; constexpr auto kODMMaxDuration = std::chrono::seconds{3600}; constexpr auto kBlacklistPath = "/api/blacklist"; constexpr auto kWhitelistPath = "/api/whitelist"; constexpr auto kRidesharingPath = "/api/whitelistRideShare"; constexpr auto kInfeasible = std::numeric_limits::min(); static auto const kReqHeaders = std::map{ {"Content-Type", "application/json"}, {"Accept", "application/json"}}; using service_times_t = std::vector>; struct direct_ride { nigiri::unixtime_t dep_; nigiri::unixtime_t arr_; }; struct capacities { std::int64_t wheelchairs_; std::int64_t bikes_; std::int64_t passengers_; std::int64_t luggage_; }; void tag_invoke(boost::json::value_from_tag const&, boost::json::value&, capacities const&); struct prima { prima(std::string const& prima_url, osr::location const& from, osr::location const& to, api::plan_params const& query); void init(nigiri::interval const& search_intvl, nigiri::interval const& taxi_intvl, bool use_first_mile_taxi, bool use_last_mile_taxi, bool use_direct_taxi, bool use_first_mile_ride_sharing, bool use_last_mile_ride_sharing, bool use_direct_ride_sharing, nigiri::timetable const& tt, nigiri::rt_timetable const* rtt, ep::routing const& r, elevators const* e, gbfs::gbfs_routing_data& gbfs, api::Place const& from, api::Place const& to, api::plan_params const& query, nigiri::routing::query const& n_query, unsigned api_version); std::size_t n_ride_sharing_events() const; std::string make_blacklist_taxi_request( nigiri::timetable const&, nigiri::interval const&) const; bool consume_blacklist_taxi_response(std::string_view json); bool blacklist_taxi(nigiri::timetable const&, nigiri::interval const&); std::string make_whitelist_taxi_request( std::vector const& first_mile, std::vector const& last_mile, nigiri::timetable const&) const; bool consume_whitelist_taxi_response( std::string_view json, std::vector&, std::vector& first_mile_taxi_rides, std::vector& last_mile_taxi_rides); bool whitelist_taxi(std::vector&, nigiri::timetable const&); std::string make_ride_sharing_request(nigiri::timetable const&) const; bool consume_ride_sharing_response(std::string_view json); bool whitelist_ride_sharing(nigiri::timetable const&); void extract_taxis_for_persisting( std::vector const& journeys); api::plan_params const& query_; boost::urls::url taxi_blacklist_; boost::urls::url taxi_whitelist_; boost::urls::url ride_sharing_whitelist_; osr::location const from_; osr::location const to_; nigiri::event_type fixed_; capacities cap_; std::optional direct_duration_; std::vector first_mile_taxi_{}; std::vector last_mile_taxi_{}; std::vector first_mile_taxi_times_{}; std::vector last_mile_taxi_times_{}; std::vector direct_taxi_{}; std::vector first_mile_ride_sharing_{}; nigiri::vecvec first_mile_ride_sharing_tour_ids_{}; std::vector last_mile_ride_sharing_{}; nigiri::vecvec last_mile_ride_sharing_tour_ids_{}; std::vector direct_ride_sharing_{}; nigiri::vecvec direct_ride_sharing_tour_ids_{}; std::vector whitelist_first_mile_locations_; std::vector whitelist_last_mile_locations_; boost::json::object whitelist_response_; }; void extract_taxis(std::vector const&, std::vector& first_mile_taxi_rides, std::vector& last_mile_taxi_rides); void fix_first_mile_duration( std::vector& journeys, std::vector const& first_mile, std::vector const& prev_first_mile, nigiri::transport_mode_id_t mode); void fix_last_mile_duration( std::vector& journeys, std::vector const& last_mile, std::vector const& prev_last_mile, nigiri::transport_mode_id_t mode); std::int64_t to_millis(nigiri::unixtime_t); nigiri::unixtime_t to_unix(std::int64_t); std::size_t n_rides_in_response(boost::json::array const&); std::string make_whitelist_request( osr::location const& from, osr::location const& to, std::vector const& first_mile, std::vector const& last_mile, std::vector const& direct, nigiri::event_type fixed, capacities const&, nigiri::timetable const&); void add_direct_odm(std::vector const&, std::vector&, place_t const& from, place_t const& to, bool arrive_by, nigiri::transport_mode_id_t); } // namespace motis::odm ================================================ FILE: include/motis/odm/query_factory.h ================================================ #pragma once #include #include "nigiri/routing/query.h" namespace motis::odm { struct query_factory { static constexpr auto const kMaxSubQueries = 9U; std::vector make_queries( bool with_taxi, bool with_ride_sharing) const; private: nigiri::routing::query make( std::vector const& start, nigiri::hash_map> const& td_start, std::vector const& dest, nigiri::hash_map> const& td_dest) const; public: // invariants nigiri::routing::query base_query_; // offsets std::vector start_walk_; std::vector dest_walk_; nigiri::hash_map> td_start_walk_; nigiri::hash_map> td_dest_walk_; nigiri::hash_map> start_taxi_short_; nigiri::hash_map> start_taxi_long_; nigiri::hash_map> dest_taxi_short_; nigiri::hash_map> dest_taxi_long_; nigiri::hash_map> start_ride_sharing_; nigiri::hash_map> dest_ride_sharing_; }; } // namespace motis::odm ================================================ FILE: include/motis/odm/shorten.h ================================================ #pragma once namespace motis::odm { void shorten(std::vector& odm_journeys, std::vector const& first_mile_taxi, std::vector const& first_mile_taxi_times, std::vector const& last_mile_taxi, std::vector const& last_mile_taxi_times, nigiri::timetable const&, nigiri::rt_timetable const*, api::plan_params const&); } // namespace motis::odm ================================================ FILE: include/motis/odm/td_offsets.h ================================================ #pragma once #include "nigiri/routing/query.h" #include "nigiri/routing/start_times.h" #include "nigiri/types.h" #include "motis/odm/prima.h" namespace motis::odm { nigiri::routing::td_offsets_t get_td_offsets( auto const& rides, nigiri::transport_mode_id_t const mode) { using namespace std::chrono_literals; auto td_offsets = nigiri::routing::td_offsets_t{}; utl::equal_ranges_linear( rides, [](auto const& a, auto const& b) { return a.stop_ == b.stop_; }, [&](auto&& from_it, auto&& to_it) { td_offsets.emplace(from_it->stop_, std::vector{}); for (auto const& r : nigiri::it_range{from_it, to_it}) { auto const tdo = nigiri::routing::td_offset{ .valid_from_ = std::min(r.time_at_stop_, r.time_at_start_), .duration_ = std::chrono::abs(r.time_at_stop_ - r.time_at_start_), .transport_mode_id_ = mode}; auto i = std::lower_bound(begin(td_offsets.at(r.stop_)), end(td_offsets.at(r.stop_)), tdo, [](auto const& a, auto const& b) { return a.valid_from_ < b.valid_from_; }); if (i == end(td_offsets.at(r.stop_)) || tdo.valid_from_ < i->valid_from_) { i = td_offsets.at(r.stop_).insert(i, tdo); } else if (tdo.duration_ < i->duration_) { *i = tdo; } if (i + 1 == end(td_offsets.at(r.stop_)) || (i + 1)->valid_from_ != tdo.valid_from_ + 1min) { td_offsets.at(r.stop_).insert( i + 1, {.valid_from_ = tdo.valid_from_ + 1min, .duration_ = nigiri::footpath::kMaxDuration, .transport_mode_id_ = mode}); } } }); for (auto& [l, tdos] : td_offsets) { for (auto i = begin(tdos); i != end(tdos);) { if (begin(tdos) < i && (i - 1)->duration_ == i->duration_ && (i - 1)->transport_mode_id_ == i->transport_mode_id_) { i = tdos.erase(i); } else { ++i; } } } return td_offsets; } std::pair get_td_offsets_split(std::vector const&, std::vector const&, nigiri::transport_mode_id_t); } // namespace motis::odm ================================================ FILE: include/motis/osr/max_distance.h ================================================ #pragma once #include #include "osr/routing/profile.h" namespace motis { double get_max_distance(osr::search_profile, std::chrono::seconds); } // namespace motis ================================================ FILE: include/motis/osr/mode_to_profile.h ================================================ #pragma once #include "osr/routing/mode.h" #include "osr/routing/profile.h" #include "motis-api/motis-api.h" namespace motis { api::ModeEnum to_mode(osr::mode); osr::search_profile to_profile(api::ModeEnum, api::PedestrianProfileEnum, api::ElevationCostsEnum); } // namespace motis ================================================ FILE: include/motis/osr/parameters.h ================================================ #pragma once #include "osr/routing/parameters.h" #include "osr/routing/profile.h" #include "motis-api/motis-api.h" namespace motis { struct osr_parameters { constexpr static auto const kFootSpeed = 1.2F; constexpr static auto const kWheelchairSpeed = 0.8F; constexpr static auto const kBikeSpeed = 4.2F; float const pedestrian_speed_{kFootSpeed}; float const cycling_speed_{kBikeSpeed}; bool const use_wheelchair_{false}; }; osr_parameters get_osr_parameters(api::plan_params const&); osr_parameters get_osr_parameters(api::oneToAll_params const&); osr_parameters get_osr_parameters(api::oneToMany_params const&); osr_parameters get_osr_parameters(api::OneToManyParams const&); osr_parameters get_osr_parameters(api::oneToManyIntermodal_params const&); osr_parameters get_osr_parameters(api::OneToManyIntermodalParams const&); osr::profile_parameters to_profile_parameters(osr::search_profile, osr_parameters const&); } // namespace motis ================================================ FILE: include/motis/osr/street_routing.h ================================================ #pragma once #include #include "osr/location.h" #include "osr/routing/profile.h" #include "osr/routing/route.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" #include "motis/types.h" namespace motis { using transport_mode_t = std::uint32_t; struct output { output() = default; virtual ~output() = default; output(output const&) = default; output(output&&) = default; output& operator=(output const&) = default; output& operator=(output&&) = default; virtual api::ModeEnum get_mode() const = 0; virtual osr::search_profile get_profile() const = 0; virtual bool is_time_dependent() const = 0; virtual transport_mode_t get_cache_key() const = 0; virtual osr::sharing_data const* get_sharing_data() const = 0; virtual void annotate_leg(nigiri::lang_t const&, osr::node_idx_t from_node, osr::node_idx_t to_node, api::Leg&) const = 0; virtual api::Place get_place(nigiri::lang_t const&, osr::node_idx_t, std::optional const& tz) const = 0; }; struct default_output final : public output { default_output(osr::ways const&, osr::search_profile); default_output(osr::ways const&, nigiri::transport_mode_id_t); ~default_output() override; bool is_time_dependent() const override; api::ModeEnum get_mode() const override; osr::search_profile get_profile() const override; transport_mode_t get_cache_key() const override; osr::sharing_data const* get_sharing_data() const override; void annotate_leg(nigiri::lang_t const&, osr::node_idx_t, osr::node_idx_t, api::Leg&) const override; api::Place get_place(nigiri::lang_t const&, osr::node_idx_t, std::optional const& tz) const override; osr::ways const& w_; osr::search_profile profile_; nigiri::transport_mode_id_t id_; }; using street_routing_cache_key_t = std:: tuple; using street_routing_cache_t = hash_map>; api::Itinerary dummy_itinerary(api::Place const& from, api::Place const& to, api::ModeEnum, nigiri::unixtime_t const start_time, nigiri::unixtime_t const end_time); api::Itinerary street_routing(osr::ways const&, osr::lookup const&, elevators const*, osr::elevation_storage const*, nigiri::lang_t const& lang, api::Place const& from, api::Place const& to, output const&, std::optional start_time, std::optional end_time, double max_matching_distance, osr_parameters const&, street_routing_cache_t&, osr::bitvec& blocked_mem, unsigned api_version, bool detailed_leg = true, std::chrono::seconds max = std::chrono::seconds{ 3600}); } // namespace motis ================================================ FILE: include/motis/parse_location.h ================================================ #pragma once #include #include #include #include "osr/routing/route.h" #include "nigiri/routing/query.h" #include "nigiri/types.h" namespace motis { std::optional parse_location(std::string_view, char separator = ','); date::sys_days parse_iso_date(std::string_view); nigiri::routing::query cursor_to_query(std::string_view); std::pair parse_cursor(std::string_view); } // namespace motis ================================================ FILE: include/motis/place.h ================================================ #pragma once #include "osr/location.h" #include "nigiri/types.h" #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis { struct tt_location { explicit tt_location(nigiri::rt::run_stop const& stop); explicit tt_location( nigiri::location_idx_t l, nigiri::location_idx_t scheduled = nigiri::location_idx_t::invalid()); friend std::ostream& operator<<(std::ostream& out, tt_location const& l) { return out << "{ l=" << l.l_ << ", scheduled=" << l.scheduled_ << " }"; } nigiri::location_idx_t l_; nigiri::location_idx_t scheduled_; }; using place_t = std::variant; inline std::ostream& operator<<(std::ostream& out, place_t const p) { return std::visit([&](auto const l) -> std::ostream& { return out << l; }, p); } osr::level_t get_lvl(osr::ways const*, osr::platforms const*, platform_matches_t const*, nigiri::location_idx_t); api::Place to_place(osr::location, std::string_view name, std::optional const& tz); api::Place to_place( nigiri::timetable const*, tag_lookup const*, osr::ways const*, osr::platforms const*, platform_matches_t const*, adr_ext const*, tz_map_t const*, nigiri::lang_t const&, place_t, place_t start = osr::location{}, place_t dest = osr::location{}, std::string_view name = "", std::optional const& fallback_tz = std::nullopt); api::Place to_place(nigiri::timetable const*, tag_lookup const*, osr::ways const*, osr::platforms const*, platform_matches_t const*, adr_ext const* ae, tz_map_t const* tz, nigiri::lang_t const&, nigiri::rt::run_stop const&, place_t start = osr::location{}, place_t dest = osr::location{}); osr::location get_location(api::Place const&); osr::location get_location(nigiri::timetable const*, osr::ways const*, osr::platforms const*, platform_matches_t const*, place_t const loc, place_t const start = {}, place_t const dest = {}); place_t get_place(nigiri::timetable const*, tag_lookup const*, std::string_view user_input); } // namespace motis ================================================ FILE: include/motis/point_rtree.h ================================================ #pragma once #include #include "cista/strong.h" #include "rtree.h" #include "geo/box.h" #include "geo/latlng.h" namespace motis { template concept RtreePosHandler = requires(geo::latlng const& pos, T const x, Fn&& f) { { std::forward(f)(pos, x) }; }; template struct point_rtree { point_rtree() : rtree_{rtree_new()} {} ~point_rtree() { if (rtree_ != nullptr) { rtree_free(rtree_); } } point_rtree(point_rtree const& o) { if (this != &o) { if (rtree_ != nullptr) { rtree_free(rtree_); } rtree_ = rtree_clone(o.rtree_); } } point_rtree(point_rtree&& o) { if (this != &o) { rtree_ = o.rtree_; o.rtree_ = nullptr; } } point_rtree& operator=(point_rtree const& o) { if (this != &o) { if (rtree_ != nullptr) { rtree_free(rtree_); } rtree_ = rtree_clone(o.rtree_); } return *this; } point_rtree& operator=(point_rtree&& o) { if (this != &o) { rtree_ = o.rtree_; o.rtree_ = nullptr; } return *this; } void add(geo::latlng const& pos, T const t) { auto const min_corner = std::array{pos.lng(), pos.lat()}; rtree_insert( rtree_, min_corner.data(), nullptr, reinterpret_cast(static_cast(cista::to_idx(t)))); } void remove(geo::latlng const& pos, T const t) { auto const min_corner = std::array{pos.lng(), pos.lat()}; rtree_delete( rtree_, min_corner.data(), nullptr, reinterpret_cast(static_cast(cista::to_idx(t)))); } std::vector in_radius(geo::latlng const& x, double distance) const { auto ret = std::vector{}; in_radius(x, distance, [&](auto&& item) { ret.emplace_back(item); }); return ret; } template void in_radius(geo::latlng const& x, double distance, Fn&& fn) const { find(geo::box{x, distance}, [&](geo::latlng const& pos, T const item) { if (geo::distance(x, pos) < distance) { fn(item); } }); } template void find(geo::box const& b, Fn&& fn) const { auto const min = b.min_.lnglat(); auto const max = b.max_.lnglat(); rtree_search( rtree_, min.data(), max.data(), [](double const* pos, double const* /* max */, void const* item, void* udata) { if constexpr (RtreePosHandler) { (*reinterpret_cast(udata))( geo::latlng{pos[1], pos[0]}, T{static_cast>( reinterpret_cast(item))}); } else { (*reinterpret_cast(udata))(T{static_cast>( reinterpret_cast(item))}); } return true; }, &fn); } rtree* rtree_{nullptr}; }; } // namespace motis ================================================ FILE: include/motis/polyline.h ================================================ #pragma once #include "geo/polyline.h" #include "motis-api/motis-api.h" namespace motis { template api::EncodedPolyline to_polyline(geo::polyline const& polyline); api::EncodedPolyline empty_polyline(); } // namespace motis ================================================ FILE: include/motis/railviz.h ================================================ #pragma once #include #include "motis-api/motis-api.h" #include "motis/fwd.h" #include "motis/match_platforms.h" namespace motis { struct railviz_static_index { railviz_static_index(nigiri::timetable const&, nigiri::shapes_storage const*); ~railviz_static_index(); struct impl; std::unique_ptr impl_; }; struct railviz_rt_index { railviz_rt_index(nigiri::timetable const&, nigiri::rt_timetable const&); ~railviz_rt_index(); struct impl; std::unique_ptr impl_; }; api::trips_response get_trains(tag_lookup const&, nigiri::timetable const&, nigiri::rt_timetable const*, nigiri::shapes_storage const*, osr::ways const*, osr::platforms const*, platform_matches_t const*, adr_ext const*, tz_map_t const*, railviz_static_index::impl const&, railviz_rt_index::impl const&, api::trips_params const&, unsigned api_version); api::routes_response get_routes(tag_lookup const&, nigiri::timetable const&, nigiri::rt_timetable const*, nigiri::shapes_storage const*, osr::ways const*, osr::platforms const*, platform_matches_t const*, adr_ext const*, tz_map_t const*, railviz_static_index::impl const&, railviz_rt_index::impl const&, api::routes_params const&, unsigned api_version); api::routeDetails_response get_route_details(tag_lookup const&, nigiri::timetable const&, nigiri::rt_timetable const*, nigiri::shapes_storage const*, osr::ways const*, osr::platforms const*, platform_matches_t const*, adr_ext const*, tz_map_t const*, railviz_static_index::impl const&, railviz_rt_index::impl const&, api::routeDetails_params const&, unsigned api_version); } // namespace motis ================================================ FILE: include/motis/route_shapes.h ================================================ #pragma once #include #include #include #include "boost/json/fwd.hpp" #include "cista/containers/pair.h" #include "cista/containers/vector.h" #include "lmdb/lmdb.hpp" #include "geo/box.h" #include "nigiri/types.h" #include "osr/routing/profile.h" #include "motis/config.h" #include "motis/fwd.h" namespace motis { struct shape_cache_entry { bool valid() const { return shape_idx_ != nigiri::scoped_shape_idx_t::invalid(); } nigiri::scoped_shape_idx_t shape_idx_{nigiri::scoped_shape_idx_t::invalid()}; cista::offset::vector offsets_; geo::box route_bbox_; cista::offset::vector segment_bboxes_; }; using shape_cache_key = cista::offset::pair>; struct shape_cache { explicit shape_cache(std::filesystem::path const&, mdb_size_t = sizeof(void*) >= 8 ? 256ULL * 1024ULL * 1024ULL * 1024ULL : 256U * 1024U * 1024U); ~shape_cache(); std::optional get(shape_cache_key const&); void put(shape_cache_key const&, shape_cache_entry const&); void sync(); lmdb::env env_; std::chrono::time_point last_sync_; }; boost::json::object route_shape_debug(osr::ways const&, osr::lookup const&, nigiri::timetable const&, nigiri::route_idx_t); void route_shapes(osr::ways const&, osr::lookup const&, nigiri::timetable const&, nigiri::shapes_storage&, config::timetable::route_shapes const&, std::array const&, shape_cache*); } // namespace motis ================================================ FILE: include/motis/rt/auser.h ================================================ #pragma once #include #include "nigiri/rt/vdv_aus.h" namespace motis { struct auser { auser(nigiri::timetable const&, nigiri::source_idx_t, nigiri::rt::vdv_aus::updater::xml_format); std::string fetch_url(std::string_view base_url); nigiri::rt::vdv_aus::statistics consume_update(std::string const&, nigiri::rt_timetable&, bool inplace = false); std::chrono::nanoseconds::rep update_state_{0}; nigiri::rt::vdv_aus::updater upd_; }; } // namespace motis ================================================ FILE: include/motis/rt/rt_metrics.h ================================================ #pragma once #include "prometheus/counter.h" #include "prometheus/family.h" #include "prometheus/gauge.h" #include "motis/metrics_registry.h" namespace motis { struct rt_metric_families { explicit rt_metric_families(prometheus::Registry& registry) : gtfsrt_updates_requested_{prometheus::BuildCounter() .Name("nigiri_gtfsrt_updates_requested_" "total") .Help("Number of update attempts of the " "GTFS-RT feed") .Register(registry)}, gtfsrt_updates_successful_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_updates_successful_total") .Help("Number of successful updates of the GTFS-RT feed") .Register(registry)}, gtfsrt_updates_error_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_updates_error_total") .Help("Number of failed updates of the GTFS-RT feed") .Register(registry)}, gtfsrt_total_entities_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_total_entities_total") .Help("Total number of entities in the GTFS-RT feed") .Register(registry)}, gtfsrt_total_entities_success_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_total_entities_success_total") .Help("Number of entities in the GTFS-RT feed that were " "successfully processed") .Register(registry)}, gtfsrt_total_entities_fail_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_total_entities_fail_total") .Help("Number of entities in the GTFS-RT feed that could not " "be processed") .Register(registry)}, gtfsrt_unsupported_deleted_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_unsupported_deleted_total") .Help("Number of unsupported deleted entities in the GTFS-RT " "feed") .Register(registry)}, gtfsrt_unsupported_vehicle_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_unsupported_vehicle_total") .Help("Number of unsupported vehicle entities in the GTFS-RT " "feed") .Register(registry)}, gtfsrt_unsupported_alert_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_unsupported_alert_total") .Help( "Number of unsupported alert entities in the GTFS-RT feed") .Register(registry)}, gtfsrt_unsupported_no_trip_id_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_unsupported_no_trip_id_total") .Help("Number of unsupported trips without trip id in the " "GTFS-RT feed") .Register(registry)}, gtfsrt_no_trip_update_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_no_trip_update_total") .Help("Number of unsupported trips without trip update in the " "GTFS-RT feed") .Register(registry)}, gtfsrt_trip_update_without_trip_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_trip_update_without_trip_total") .Help("Number of unsupported trip updates without trip in the " "GTFS-RT feed") .Register(registry)}, gtfsrt_trip_resolve_error_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_trip_resolve_error_total") .Help("Number of unresolved trips in the GTFS-RT feed") .Register(registry)}, gtfsrt_unsupported_schedule_relationship_{ prometheus::BuildCounter() .Name("nigiri_gtfsrt_unsupported_schedule_relationship_total") .Help("Number of unsupported schedule relationships in the " "GTFS-RT feed") .Register(registry)}, gtfsrt_feed_timestamp_{prometheus::BuildGauge() .Name("nigiri_gtfsrt_feed_timestamp_seconds") .Help("Timestamp of the GTFS-RT feed") .Register(registry)}, gtfsrt_last_update_timestamp_{ prometheus::BuildGauge() .Name("nigiri_gtfsrt_last_update_timestamp_seconds") .Help("Last update timestamp of the GTFS-RT feed") .Register(registry)}, vdvaus_updates_requested_{prometheus::BuildCounter() .Name("nigiri_vdvaus_updates_requested_" "total") .Help("Number of update attempts of the " "VDV AUS feed") .Register(registry)}, vdvaus_updates_successful_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_updates_successful_total") .Help("Number of successful updates of the VDV AUS feed") .Register(registry)}, vdvaus_updates_error_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_updates_error_total") .Help("Number of failed updates of the VDV AUS feed") .Register(registry)}, vdvaus_unsupported_additional_runs_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_unsupported_additional_runs_total") .Help("Number of unsupported additional runs in the VDV AUS " "feed") .Register(registry)}, vdvaus_unsupported_additional_stops_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_unsupported_additional_runs_total") .Help("Number of additional stops in the VDV AUS feed") .Register(registry)}, vdvaus_current_matches_total_{ prometheus::BuildGauge() .Name("nigiri_vdvaus_current_matches_total") .Help("Current number of unique run IDs for which matching " "was performed") .Register(registry)}, vdvaus_current_matches_non_empty_{ prometheus::BuildGauge() .Name("nigiri_vdvaus_current_matches_non_empty_total") .Help("Current number of unique run IDs for which a matching " "was performed and a non-empty result was achieved") .Register(registry)}, vdvaus_total_runs_{prometheus::BuildCounter() .Name("nigiri_vdvaus_total_runs_total") .Help("Total number of runs in the VDV AUS feed") .Register(registry)}, vdvaus_complete_runs_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_complete_runs_total") .Help("Total number of complete runs in the VDV AUS feed") .Register(registry)}, vdvaus_unique_runs_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_unique_runs_total") .Help("Total number of unique runs in the VDV AUS feed") .Register(registry)}, vdvaus_match_attempts_{prometheus::BuildCounter() .Name("nigiri_vdvaus_match_attempts_total") .Help("Total number of match attempts") .Register(registry)}, vdvaus_matched_runs_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_matched_runs_total") .Help("Number of runs of the VDV AUS feed for which a " "successful match attempt took place") .Register(registry)}, vdvaus_found_runs_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_found_runs_total") .Help("Number of runs of the VDV AUS feed for which a matching " "run in the static timetable could be looked up " "successfully") .Register(registry)}, vdvaus_multiple_matches_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_multiple_matches_total") .Help("Number of times a run of the VDV AUS feed could not be " "matched to a transport in the timetable since there " "were multiple transports with the same score") .Register(registry)}, vdvaus_incomplete_not_seen_before_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_incomplete_not_seen_before_total") .Help( "Number of times an incomplete run was encountered before " "seeing a complete version of it in the VDV AUS feed") .Register(registry)}, vdvaus_complete_after_incomplete_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_complete_after_incomplete_total") .Help("Number of times a complete run was encountered in the " "feed after seeing an incomplete version before") .Register(registry)}, vdvaus_no_transport_found_at_stop_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_no_transport_found_at_stop_total") .Help("Number of times that no transport could be found at the " "stop specified in the VDV AUS feed") .Register(registry)}, vdvaus_total_stops_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_total_stops_total") .Help("Total number of stops in the VDV AUS feed") .Register(registry)}, vdvaus_resolved_stops_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_resolved_stops_total") .Help("Number of stops that could be resolved to locations in " "the timetable") .Register(registry)}, vdvaus_runs_without_stops_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_runs_without_stops_total") .Help("Number of times a run without any stops was encountered " "in the VDV AUS feed") .Register(registry)}, vdvaus_cancelled_runs_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_cancelled_runs_total") .Help("Number of cancelled runs in the VDV AUS feed") .Register(registry)}, vdvaus_skipped_vdv_stops_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_skipped_vdv_stops_total") .Help("Number of stops in the VDV AUS feed that had to be " "skipped while updating a run since they had no " "counterpart in the run of the timetable") .Register(registry)}, vdvaus_excess_vdv_stops_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_excess_vdv_stops_total") .Help( "Number of additional stops at the end of runs in VDV AUS " "feed that had no corresponding stop in the run of the " "timetable that was updated") .Register(registry)}, vdvaus_updated_events_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_updated_events_total") .Help("Number of arrival/departure times " "that were updated by the VDV AUS feed") .Register(registry)}, vdvaus_propagated_delays_{ prometheus::BuildCounter() .Name("nigiri_vdvaus_propagated_delays_total") .Help("Number of delay propagations by the VDV AUS feed") .Register(registry)}, vdvaus_feed_timestamp_{prometheus::BuildGauge() .Name("nigiri_vdvaus_feed_timestamp_seconds") .Help("Timestamp of the VDV AUS feed") .Register(registry)}, vdvaus_last_update_timestamp_{ prometheus::BuildGauge() .Name("nigiri_vdvaus_last_update_timestamp_seconds") .Help("Last update timestamp of the VDV AUS feed") .Register(registry)} {} prometheus::Family& gtfsrt_updates_requested_; prometheus::Family& gtfsrt_updates_successful_; prometheus::Family& gtfsrt_updates_error_; prometheus::Family& gtfsrt_total_entities_; prometheus::Family& gtfsrt_total_entities_success_; prometheus::Family& gtfsrt_total_entities_fail_; prometheus::Family& gtfsrt_unsupported_deleted_; prometheus::Family& gtfsrt_unsupported_vehicle_; prometheus::Family& gtfsrt_unsupported_alert_; prometheus::Family& gtfsrt_unsupported_no_trip_id_; prometheus::Family& gtfsrt_no_trip_update_; prometheus::Family& gtfsrt_trip_update_without_trip_; prometheus::Family& gtfsrt_trip_resolve_error_; prometheus::Family& gtfsrt_unsupported_schedule_relationship_; prometheus::Family& gtfsrt_feed_timestamp_; prometheus::Family& gtfsrt_last_update_timestamp_; prometheus::Family& vdvaus_updates_requested_; prometheus::Family& vdvaus_updates_successful_; prometheus::Family& vdvaus_updates_error_; prometheus::Family& vdvaus_unsupported_additional_runs_; prometheus::Family& vdvaus_unsupported_additional_stops_; prometheus::Family& vdvaus_current_matches_total_; prometheus::Family& vdvaus_current_matches_non_empty_; prometheus::Family& vdvaus_total_runs_; prometheus::Family& vdvaus_complete_runs_; prometheus::Family& vdvaus_unique_runs_; prometheus::Family& vdvaus_match_attempts_; prometheus::Family& vdvaus_matched_runs_; prometheus::Family& vdvaus_found_runs_; prometheus::Family& vdvaus_multiple_matches_; prometheus::Family& vdvaus_incomplete_not_seen_before_; prometheus::Family& vdvaus_complete_after_incomplete_; prometheus::Family& vdvaus_no_transport_found_at_stop_; prometheus::Family& vdvaus_total_stops_; prometheus::Family& vdvaus_resolved_stops_; prometheus::Family& vdvaus_runs_without_stops_; prometheus::Family& vdvaus_cancelled_runs_; prometheus::Family& vdvaus_skipped_vdv_stops_; prometheus::Family& vdvaus_excess_vdv_stops_; prometheus::Family& vdvaus_updated_events_; prometheus::Family& vdvaus_propagated_delays_; prometheus::Family& vdvaus_feed_timestamp_; prometheus::Family& vdvaus_last_update_timestamp_; }; struct gtfsrt_metrics { explicit gtfsrt_metrics(std::string const& tag, rt_metric_families const& m) : updates_requested_{m.gtfsrt_updates_requested_.Add({{"tag", tag}})}, updates_successful_{m.gtfsrt_updates_successful_.Add({{"tag", tag}})}, updates_error_{m.gtfsrt_updates_error_.Add({{"tag", tag}})}, total_entities_{m.gtfsrt_total_entities_.Add({{"tag", tag}})}, total_entities_success_{ m.gtfsrt_total_entities_success_.Add({{"tag", tag}})}, total_entities_fail_{m.gtfsrt_total_entities_fail_.Add({{"tag", tag}})}, unsupported_deleted_{m.gtfsrt_unsupported_deleted_.Add({{"tag", tag}})}, unsupported_vehicle_{m.gtfsrt_unsupported_vehicle_.Add({{"tag", tag}})}, unsupported_alert_{m.gtfsrt_unsupported_alert_.Add({{"tag", tag}})}, unsupported_no_trip_id_{ m.gtfsrt_unsupported_no_trip_id_.Add({{"tag", tag}})}, no_trip_update_{m.gtfsrt_no_trip_update_.Add({{"tag", tag}})}, trip_update_without_trip_{ m.gtfsrt_trip_update_without_trip_.Add({{"tag", tag}})}, trip_resolve_error_{m.gtfsrt_trip_resolve_error_.Add({{"tag", tag}})}, unsupported_schedule_relationship_{ m.gtfsrt_unsupported_schedule_relationship_.Add({{"tag", tag}})}, feed_timestamp_{m.gtfsrt_feed_timestamp_.Add({{"tag", tag}})}, last_update_timestamp_{ m.gtfsrt_last_update_timestamp_.Add({{"tag", tag}})} {} void update(nigiri::rt::statistics const& stats) const { total_entities_.Increment(stats.total_entities_); total_entities_success_.Increment(stats.total_entities_success_); total_entities_fail_.Increment(stats.total_entities_fail_); unsupported_deleted_.Increment(stats.unsupported_deleted_); unsupported_no_trip_id_.Increment(stats.unsupported_no_trip_id_); no_trip_update_.Increment(stats.no_trip_update_); trip_update_without_trip_.Increment(stats.trip_update_without_trip_); trip_resolve_error_.Increment(stats.trip_resolve_error_); unsupported_schedule_relationship_.Increment( stats.unsupported_schedule_relationship_); feed_timestamp_.Set( static_cast(stats.feed_timestamp_.time_since_epoch().count())); } prometheus::Counter& updates_requested_; prometheus::Counter& updates_successful_; prometheus::Counter& updates_error_; prometheus::Counter& total_entities_; prometheus::Counter& total_entities_success_; prometheus::Counter& total_entities_fail_; prometheus::Counter& unsupported_deleted_; prometheus::Counter& unsupported_vehicle_; prometheus::Counter& unsupported_alert_; prometheus::Counter& unsupported_no_trip_id_; prometheus::Counter& no_trip_update_; prometheus::Counter& trip_update_without_trip_; prometheus::Counter& trip_resolve_error_; prometheus::Counter& unsupported_schedule_relationship_; prometheus::Gauge& feed_timestamp_; prometheus::Gauge& last_update_timestamp_; }; struct vdvaus_metrics { explicit vdvaus_metrics(std::string const& tag, rt_metric_families const& m) : updates_requested_{m.vdvaus_updates_requested_.Add({{"tag", tag}})}, updates_successful_{m.vdvaus_updates_successful_.Add({{"tag", tag}})}, updates_error_{m.vdvaus_updates_error_.Add({{"tag", tag}})}, unsupported_additional_runs_{ m.vdvaus_unsupported_additional_runs_.Add({{"tag", tag}})}, unsupported_additional_stops_{ m.vdvaus_unsupported_additional_stops_.Add({{"tag", tag}})}, current_matches_total_{ m.vdvaus_current_matches_total_.Add({{"tag", tag}})}, current_matches_non_empty_{ m.vdvaus_current_matches_non_empty_.Add({{"tag", tag}})}, total_runs_{m.vdvaus_total_runs_.Add({{"tag", tag}})}, complete_runs_{m.vdvaus_complete_runs_.Add({{"tag", tag}})}, unique_runs_{m.vdvaus_unique_runs_.Add({{"tag", tag}})}, match_attempts_{m.vdvaus_match_attempts_.Add({{"tag", tag}})}, matched_runs_{m.vdvaus_matched_runs_.Add({{"tag", tag}})}, found_runs_{m.vdvaus_found_runs_.Add({{"tag", tag}})}, multiple_matches_{m.vdvaus_multiple_matches_.Add({{"tag", tag}})}, incomplete_not_seen_before_{ m.vdvaus_incomplete_not_seen_before_.Add({{"tag", tag}})}, complete_after_incomplete_{ m.vdvaus_complete_after_incomplete_.Add({{"tag", tag}})}, no_transport_found_at_stop_{ m.vdvaus_no_transport_found_at_stop_.Add({{"tag", tag}})}, total_stops_{m.vdvaus_total_stops_.Add({{"tag", tag}})}, resolved_stops_{m.vdvaus_resolved_stops_.Add({{"tag", tag}})}, runs_without_stops_{m.vdvaus_runs_without_stops_.Add({{"tag", tag}})}, cancelled_runs_{m.vdvaus_cancelled_runs_.Add({{"tag", tag}})}, skipped_vdv_stops_{m.vdvaus_skipped_vdv_stops_.Add({{"tag", tag}})}, excess_vdv_stops_{m.vdvaus_excess_vdv_stops_.Add({{"tag", tag}})}, updated_events_{m.vdvaus_updated_events_.Add({{"tag", tag}})}, propagated_delays_{m.vdvaus_propagated_delays_.Add({{"tag", tag}})}, last_update_timestamp_{ m.vdvaus_last_update_timestamp_.Add({{"tag", tag}})} {} void update(nigiri::rt::vdv_aus::statistics const& stats) const { unsupported_additional_runs_.Increment(stats.unsupported_additional_runs_); unsupported_additional_stops_.Increment( stats.unsupported_additional_stops_); current_matches_total_.Set( static_cast(stats.current_matches_total_)); current_matches_non_empty_.Set(stats.current_matches_non_empty_); total_runs_.Increment(stats.total_runs_); complete_runs_.Increment(stats.complete_runs_); unique_runs_.Increment(stats.unique_runs_); match_attempts_.Increment(stats.match_attempts_); matched_runs_.Increment(stats.matched_runs_); found_runs_.Increment(stats.found_runs_); multiple_matches_.Increment(stats.multiple_matches_); incomplete_not_seen_before_.Increment(stats.incomplete_not_seen_before_); complete_after_incomplete_.Increment(stats.complete_after_incomplete_); no_transport_found_at_stop_.Increment(stats.no_transport_found_at_stop_); total_stops_.Increment(stats.total_stops_); resolved_stops_.Increment(stats.resolved_stops_); runs_without_stops_.Increment(stats.runs_without_stops_); cancelled_runs_.Increment(stats.cancelled_runs_); skipped_vdv_stops_.Increment(stats.skipped_vdv_stops_); excess_vdv_stops_.Increment(stats.excess_vdv_stops_); updated_events_.Increment(stats.updated_events_); propagated_delays_.Increment(stats.propagated_delays_); } prometheus::Counter& updates_requested_; prometheus::Counter& updates_successful_; prometheus::Counter& updates_error_; prometheus::Counter& unsupported_additional_runs_; prometheus::Counter& unsupported_additional_stops_; prometheus::Gauge& current_matches_total_; prometheus::Gauge& current_matches_non_empty_; prometheus::Counter& total_runs_; prometheus::Counter& complete_runs_; prometheus::Counter& unique_runs_; prometheus::Counter& match_attempts_; prometheus::Counter& matched_runs_; prometheus::Counter& found_runs_; prometheus::Counter& multiple_matches_; prometheus::Counter& incomplete_not_seen_before_; prometheus::Counter& complete_after_incomplete_; prometheus::Counter& no_transport_found_at_stop_; prometheus::Counter& total_stops_; prometheus::Counter& resolved_stops_; prometheus::Counter& runs_without_stops_; prometheus::Counter& cancelled_runs_; prometheus::Counter& skipped_vdv_stops_; prometheus::Counter& excess_vdv_stops_; prometheus::Counter& updated_events_; prometheus::Counter& propagated_delays_; prometheus::Gauge& last_update_timestamp_; }; } // namespace motis ================================================ FILE: include/motis/rt_update.h ================================================ #pragma once #include #include #include "boost/asio/io_context.hpp" #include "motis/fwd.h" namespace motis { void run_rt_update(boost::asio::io_context&, config const&, data&); } ================================================ FILE: include/motis/server.h ================================================ #pragma once #include #include "boost/url/url_view.hpp" namespace motis { struct data; struct config; int server(data d, config const& c, std::string_view); unsigned get_api_version(boost::urls::url_view const&); } // namespace motis ================================================ FILE: include/motis/tag_lookup.h ================================================ #pragma once #include #include #include #include "cista/memory_holder.h" #include "nigiri/types.h" #include "motis/fwd.h" namespace motis { template struct trip_id { T start_date_; T start_time_; T tag_; T trip_id_; }; trip_id split_trip_id(std::string_view); struct tag_lookup { void add(nigiri::source_idx_t, std::string_view str); nigiri::source_idx_t get_src(std::string_view tag) const; std::string_view get_tag(nigiri::source_idx_t) const; std::string id(nigiri::timetable const&, nigiri::location_idx_t) const; std::string id(nigiri::timetable const&, nigiri::rt::run_stop, nigiri::event_type) const; std::string route_id(nigiri::rt::run_stop, nigiri::event_type) const; trip_id id_fragments(nigiri::timetable const&, nigiri::rt::run_stop, nigiri::event_type const) const; nigiri::location_idx_t get_location(nigiri::timetable const&, std::string_view) const; std::optional find_location(nigiri::timetable const&, std::string_view) const; std::pair get_trip( nigiri::timetable const&, nigiri::rt_timetable const*, std::string_view) const; friend std::ostream& operator<<(std::ostream&, tag_lookup const&); void write(std::filesystem::path const&) const; static cista::wrapped read(std::filesystem::path const&); nigiri::vecvec src_to_tag_; nigiri::hash_map tag_to_src_; }; } // namespace motis ================================================ FILE: include/motis/tiles_data.h ================================================ #pragma once #include #include "tiles/db/tile_database.h" #include "tiles/get_tile.h" namespace motis { struct tiles_data { tiles_data(std::string const& path, std::size_t const db_size) : db_env_{::tiles::make_tile_database(path.c_str(), db_size)}, db_handle_{db_env_}, render_ctx_{::tiles::make_render_ctx(db_handle_)}, pack_handle_{path.c_str()} {} lmdb::env db_env_; ::tiles::tile_db_handle db_handle_; ::tiles::render_ctx render_ctx_; ::tiles::pack_handle pack_handle_; }; } // namespace motis ================================================ FILE: include/motis/timetable/clasz_to_mode.h ================================================ #pragma once #include "nigiri/routing/clasz_mask.h" #include "nigiri/types.h" #include "motis-api/motis-api.h" namespace motis { api::ModeEnum to_mode(nigiri::clasz, unsigned api_version); std::vector to_modes(nigiri::routing::clasz_mask_t, unsigned api_version); } // namespace motis ================================================ FILE: include/motis/timetable/modes_to_clasz_mask.h ================================================ #pragma once #include "motis-api/motis-api.h" #include "nigiri/routing/clasz_mask.h" namespace motis { nigiri::routing::clasz_mask_t to_clasz_mask(std::vector const&); } ================================================ FILE: include/motis/timetable/time_conv.h ================================================ #pragma once #include #include #include "nigiri/types.h" namespace motis { inline std::int64_t to_seconds(nigiri::unixtime_t const t) { return std::chrono::duration_cast(t.time_since_epoch()) .count(); } inline std::int64_t to_seconds(nigiri::i32_minutes const t) { return std::chrono::duration_cast(t).count(); } inline std::int64_t to_ms(nigiri::i32_minutes const t) { return std::chrono::duration_cast(t).count(); } } // namespace motis ================================================ FILE: include/motis/transport_mode_ids.h ================================================ #pragma once #include "osr/routing/profile.h" #include "nigiri/types.h" namespace motis { constexpr auto const kOdmTransportModeId = static_cast(osr::kNumProfiles); constexpr auto const kRideSharingTransportModeId = static_cast(osr::kNumProfiles + 1U); constexpr auto const kGbfsTransportModeIdOffset = static_cast(osr::kNumProfiles + 2U); constexpr auto const kFlexModeIdOffset = static_cast(1'000'000U); } // namespace motis ================================================ FILE: include/motis/tt_location_rtree.h ================================================ #pragma once #include "nigiri/special_stations.h" #include "nigiri/timetable.h" #include "motis/point_rtree.h" namespace motis { inline point_rtree create_location_rtree( nigiri::timetable const& tt) { auto t = point_rtree{}; for (auto i = nigiri::location_idx_t{nigiri::kNSpecialStations}; i != tt.n_locations(); ++i) { t.add(tt.locations_.coordinates_[i], i); } return t; } } // namespace motis ================================================ FILE: include/motis/types.h ================================================ #pragma once #include #include #include #include #include "geo/latlng.h" #include "cista/reflection/comparable.h" #include "cista/strong.h" #include "nigiri/common/interval.h" #include "nigiri/types.h" #include "motis/elevators/get_state_changes.h" namespace nigiri { struct rt_timetable; } namespace motis { template using vector_map = nigiri::vector_map; template using hash_set = nigiri::hash_set; template using hash_map = nigiri::hash_map; template using basic_string = std::basic_string>; using elevator_idx_t = cista::strong; using gbfs_provider_idx_t = cista::strong; struct elevator { friend bool operator==(elevator const&, elevator const&) = default; std::vector> const& get_state_changes() const { return state_changes_; } std::int64_t id_; std::optional id_str_; geo::latlng pos_; bool status_; std::string desc_; std::vector> out_of_service_; std::vector> state_changes_{ intervals_to_state_changes(out_of_service_, status_)}; }; using rtt_ptr_t = std::shared_ptr; } // namespace motis ================================================ FILE: include/motis/update_rtt_td_footpaths.h ================================================ #pragma once #include #include "osr/lookup.h" #include "osr/routing/route.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/timetable.h" #include "motis/compute_footpaths.h" #include "motis/data.h" #include "motis/elevators/elevators.h" #include "motis/fwd.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" namespace motis { using nodes_t = std::vector; using states_t = std::vector; osr::bitvec& set_blocked(nodes_t const&, states_t const&, osr::bitvec&); std::vector get_td_footpaths( osr::ways const&, osr::lookup const&, osr::platforms const&, nigiri::timetable const&, nigiri::rt_timetable const*, point_rtree const&, elevators const&, platform_matches_t const&, nigiri::location_idx_t start_l, osr::location start, osr::direction, osr::search_profile, std::chrono::seconds max, double max_matching_distance, osr_parameters const&, osr::bitvec& blocked_mem); std::optional> get_states_at(osr::ways const&, osr::lookup const&, elevators const&, nigiri::unixtime_t, geo::latlng const&); void update_rtt_td_footpaths( osr::ways const&, osr::lookup const&, osr::platforms const&, nigiri::timetable const&, point_rtree const&, elevators const&, platform_matches_t const&, hash_set> const& tasks, nigiri::rt_timetable const* old_rtt, nigiri::rt_timetable&, std::chrono::seconds max); void update_rtt_td_footpaths(osr::ways const&, osr::lookup const&, osr::platforms const&, nigiri::timetable const&, point_rtree const&, elevators const&, elevator_footpath_map_t const&, platform_matches_t const&, nigiri::rt_timetable&, std::chrono::seconds max); } // namespace motis ================================================ FILE: openapi.yaml ================================================ openapi: 3.1.0 info: title: MOTIS API description: | This is the MOTIS routing API. Overview of MOTIS API versions: MOTIS 0.x - deprecated/discontinued MOTIS 2.x - current, providing: * /api/v5/{plan,trip,stoptimes,map/trips} renamed METRO mode to SUBURBAN, AREAL_LIFT to AERIAL_LIFT; since MOTIS 2.5.0 * /api/v4/{plan,trip,stoptimes,map/trips} new displayName property, routeShortName only contains actual route short name from source; since MOTIS 2.2.0 * /api/v3/plan with correct maxTransfers API parameter (transfers actually corresponding to number of changes between transit legs (and not to number of transit legs), i.e. maxTransfers=0 returns direct public transit connections, as expected); since MOTIS 2.0.84 * /api/v2/{plan,trip} returns Google polylines with precision=6; since MOTIS 2.0.60 * /api/v1/{plan,trip} returns Google polylines with precision=7 (not defined for |longitude|>107) * /api/v1/* all other endpoints If you use the JS client lib https://www.npmjs.com/package/@motis-project/motis-client, endpoint versions will be taken into account automatically (i.e. the newest one available will be used). contact: email: felix@triptix.tech license: name: MIT url: https://opensource.org/license/mit version: v5 externalDocs: description: Find out more about MOTIS url: https://github.com/motis-project/motis servers: - url: https://api.transitous.org description: Transitous production server - url: https://staging.api.transitous.org description: Transitous staging server - url: http://localhost:8080 description: Local MOTIS server paths: /api/v5/plan: get: tags: - routing summary: Computes optimal connections from one place to another. operationId: plan parameters: - name: fromPlace in: query required: true description: | \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id schema: type: string - name: toPlace in: query required: true description: | \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id schema: type: string - name: radius in: query required: false description: | Experimental. Search radius in meters around the `fromPlace` / `toPlace` coordinates. When set and the place is given as coordinates, all transit stops within this radius are used as start/end points with zero pre-transit/post-transit time. Works without OSM/street routing data loaded. schema: type: number format: double - name: via in: query required: false description: | List of via stops to visit (only stop IDs, no coordinates allowed for now). Also see the optional parameter `viaMinimumStay` to set a set a minimum stay duration for each via stop. schema: type: array maxItems: 2 items: type: string explode: false - name: viaMinimumStay in: query required: false description: | Optional. If not set, the default is `0,0` - no stay required. For each `via` stop a minimum stay duration in minutes. The value `0` signals that it's allowed to stay in the same trip. This enables via stays without counting a transfer and can lead to better connections with less transfers. Transfer connections can still be found with `viaMinimumStay=0`. schema: default: [ 0, 0 ] type: array maxItems: 2 items: type: integer explode: false - name: time in: query required: false description: | Optional. Defaults to the current time. Departure time ($arriveBy=false) / arrival date ($arriveBy=true), schema: type: string format: date-time - name: maxTransfers in: query required: false description: | The maximum number of allowed transfers (i.e. interchanges between transit legs, pre- and postTransit do not count as transfers). `maxTransfers=0` searches for direct transit connections without any transfers. If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), send an empty `transitModes` parameter instead. If not provided, the routing uses the server-side default value which is hardcoded and very high to cover all use cases. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the fastest) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. In plan endpoints before v3, the behavior is off by one, i.e. `maxTransfers=0` only returns non-transit connections. schema: type: integer - name: maxTravelTime in: query required: false description: | The maximum travel time in minutes. If not provided, the routing to uses the value hardcoded in the server which is usually quite high. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the least transfers) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. schema: type: integer - name: minTransferTime in: query required: false description: | Optional. Default is 0 minutes. Minimum transfer time for each transfer in minutes. schema: type: integer default: 0 - name: additionalTransferTime in: query required: false description: | Optional. Default is 0 minutes. Additional transfer time reserved for each transfer in minutes. schema: type: integer default: 0 - name: transferTimeFactor in: query required: false description: | Optional. Default is 1.0 Factor to multiply minimum required transfer times with. Values smaller than 1.0 are not supported. schema: type: number default: 1.0 - name: maxMatchingDistance in: query required: false description: | Optional. Default is 25 meters. Maximum matching distance in meters to match geo coordinates to the street network. schema: type: number default: 25 - name: pedestrianProfile in: query required: false description: | Optional. Default is `FOOT`. Accessibility profile to use for pedestrian routing in transfers between transit connections, on the first mile, and last mile. schema: $ref: '#/components/schemas/PedestrianProfile' default: FOOT - name: pedestrianSpeed in: query required: false description: | Optional Average speed for pedestrian routing. schema: $ref: '#/components/schemas/PedestrianSpeed' - name: cyclingSpeed in: query required: false description: | Optional Average speed for bike routing. schema: $ref: '#/components/schemas/CyclingSpeed' - name: elevationCosts in: query required: false description: | Optional. Default is `NONE`. Set an elevation cost profile, to penalize routes with incline. - `NONE`: No additional costs for elevations. This is the default behavior - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. The profile is used for direct routing, on the first mile, and last mile. Elevation cost profiles are currently used by following street modes: - `BIKE` schema: $ref: '#/components/schemas/ElevationCosts' default: NONE - name: useRoutedTransfers in: query required: false description: | Optional. Default is `false`. Whether to use transfers routed on OpenStreetMap data. schema: type: boolean default: false - name: detailedTransfers in: query required: false description: | Controls if transfer polylines and step instructions are returned. If not set, this parameter inherits the value of `detailedLegs`. - true: Compute transfer polylines and step instructions. - false: Return empty `legGeometry` and omit `steps` for transfers. schema: type: boolean - name: detailedLegs in: query required: false description: | Controls if `legGeometry` and `steps` are returned for direct legs, pre-/post-transit legs and transit legs. schema: type: boolean default: true - name: joinInterlinedLegs in: query description: | Optional. Default is `true`. Controls if a journey section with stay-seated transfers is returned: - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.). Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`. - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops schema: type: boolean default: true - name: transitModes in: query required: false description: | Optional. Default is `TRANSIT` which allows all transit modes (no restriction). Allowed modes for the transit part. If empty, no transit connections will be computed. For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. schema: default: - TRANSIT type: array items: $ref: '#/components/schemas/Mode' explode: false - name: directModes in: query required: false description: | Optional. Default is `WALK` which will compute walking routes as direct connections. Modes used for direction connections from start to destination without using transit. Results will be returned on the `direct` key. Note: Direct connections will only be returned on the first call. For paging calls, they can be omitted. Note: Transit connections that are slower than the fastest direct connection will not show up. This is being used as a cut-off during transit routing to speed up the search. To prevent this, it's possible to send two separate requests (one with only `transitModes` and one with only `directModes`). Note: the output `direct` array will stay empty if the input param `maxDirectTime` makes any direct trip impossible. Only non-transit modes such as `WALK`, `BIKE`, `CAR`, `BIKE_SHARING`, etc. can be used. schema: default: - WALK type: array items: $ref: '#/components/schemas/Mode' explode: false - name: preTransitModes in: query required: false description: | Optional. Default is `WALK`. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`). A list of modes that are allowed to be used from the `from` coordinate to the first transit stop. Example: `WALK,BIKE_SHARING`. schema: default: - WALK type: array items: $ref: '#/components/schemas/Mode' explode: false - name: postTransitModes in: query required: false description: | Optional. Default is `WALK`. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`). A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`. schema: default: - WALK type: array items: $ref: '#/components/schemas/Mode' explode: false - name: directRentalFormFactors in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies to direct connections. A list of vehicle type form factors that are allowed to be used for direct connections. If empty (the default), all form factors are allowed. Example: `BICYCLE,SCOOTER_STANDING`. schema: type: array items: $ref: '#/components/schemas/RentalFormFactor' explode: false - name: preTransitRentalFormFactors in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`). A list of vehicle type form factors that are allowed to be used from the `from` coordinate to the first transit stop. If empty (the default), all form factors are allowed. Example: `BICYCLE,SCOOTER_STANDING`. schema: type: array items: $ref: '#/components/schemas/RentalFormFactor' explode: false - name: postTransitRentalFormFactors in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`). A list of vehicle type form factors that are allowed to be used from the last transit stop to the `to` coordinate. If empty (the default), all form factors are allowed. Example: `BICYCLE,SCOOTER_STANDING`. schema: type: array items: $ref: '#/components/schemas/RentalFormFactor' explode: false - name: directRentalPropulsionTypes in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies to direct connections. A list of vehicle type form factors that are allowed to be used for direct connections. If empty (the default), all propulsion types are allowed. Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`. schema: type: array items: $ref: '#/components/schemas/RentalPropulsionType' explode: false - name: preTransitRentalPropulsionTypes in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`). A list of vehicle propulsion types that are allowed to be used from the `from` coordinate to the first transit stop. If empty (the default), all propulsion types are allowed. Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`. schema: type: array items: $ref: '#/components/schemas/RentalPropulsionType' explode: false - name: postTransitRentalPropulsionTypes in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`). A list of vehicle propulsion types that are allowed to be used from the last transit stop to the `to` coordinate. If empty (the default), all propulsion types are allowed. Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`. schema: type: array items: $ref: '#/components/schemas/RentalPropulsionType' explode: false - name: directRentalProviders in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies to direct connections. A list of rental providers that are allowed to be used for direct connections. If empty (the default), all providers are allowed. schema: type: array items: type: string explode: false - name: directRentalProviderGroups in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies to direct connections. A list of rental provider groups that are allowed to be used for direct connections. If empty (the default), all providers are allowed. schema: type: array items: type: string explode: false - name: preTransitRentalProviders in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`). A list of rental providers that are allowed to be used from the `from` coordinate to the first transit stop. If empty (the default), all providers are allowed. schema: type: array items: type: string explode: false - name: preTransitRentalProviderGroups in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`). A list of rental provider groups that are allowed to be used from the `from` coordinate to the first transit stop. If empty (the default), all providers are allowed. schema: type: array items: type: string explode: false - name: postTransitRentalProviders in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`). A list of rental providers that are allowed to be used from the last transit stop to the `to` coordinate. If empty (the default), all providers are allowed. schema: type: array items: type: string explode: false - name: postTransitRentalProviderGroups in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`). A list of rental provider groups that are allowed to be used from the last transit stop to the `to` coordinate. If empty (the default), all providers are allowed. schema: type: array items: type: string explode: false - name: ignoreDirectRentalReturnConstraints in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Default is `false`. If set to `true`, the routing will ignore rental return constraints for direct connections, allowing the rental vehicle to be parked anywhere. schema: type: boolean default: false - name: ignorePreTransitRentalReturnConstraints in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Default is `false`. If set to `true`, the routing will ignore rental return constraints for the part from the `from` coordinate to the first transit stop, allowing the rental vehicle to be parked anywhere. schema: type: boolean default: false - name: ignorePostTransitRentalReturnConstraints in: query required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Default is `false`. If set to `true`, the routing will ignore rental return constraints for the part from the last transit stop to the `to` coordinate, allowing the rental vehicle to be parked anywhere. schema: type: boolean default: false - name: numItineraries in: query required: false description: | The minimum number of itineraries to compute. This is only relevant if `timetableView=true`. The default value is 5. schema: type: integer default: 5 - name: maxItineraries in: query required: false description: | Optional. By default all computed itineraries will be returned The maximum number of itineraries to compute. This is only relevant if `timetableView=true`. Note: With the current implementation, setting this to a lower number will not result in any speedup. Note: The number of returned itineraries might be slightly higher than `maxItineraries` as there might be several itineraries with the same departure time but different number of transfers. In order to not miss any itineraries for paging, either none or all itineraries with the same departure time have to be returned. schema: type: integer - name: pageCursor in: query required: false description: | Use the cursor to go to the next "page" of itineraries. Copy the cursor from the last response and keep the original request as is. This will enable you to search for itineraries in the next or previous time-window. schema: type: string - name: timetableView in: query required: false description: | Optional. Default is `true`. Search for the best trip options within a time window. If true two itineraries are considered optimal if one is better on arrival time (earliest wins) and the other is better on departure time (latest wins). In combination with arriveBy this parameter cover the following use cases: `timetable=false` = waiting for the first transit departure/arrival is considered travel time: - `arriveBy=true`: event (e.g. a meeting) starts at 10:00 am, compute the best journeys that arrive by that time (maximizes departure time) - `arriveBy=false`: event (e.g. a meeting) ends at 11:00 am, compute the best journeys that depart after that time `timetable=true` = optimize "later departure" + "earlier arrival" and give all options over a time window: - `arriveBy=true`: the time window around `date` and `time` refers to the arrival time window - `arriveBy=false`: the time window around `date` and `time` refers to the departure time window schema: type: boolean default: true - name: arriveBy in: query required: false schema: type: boolean default: false description: | Optional. Default is `false`. - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time - `arriveBy=false`: the parameters `date` and `time` refer to the departure time - name: searchWindow in: query required: false description: | Optional. Default is 15 minutes which is `900`. The length of the search-window in seconds. Default value 15 minutes. - `arriveBy=true`: number of seconds between the earliest departure time and latest departure time - `arriveBy=false`: number of seconds between the earliest arrival time and the latest arrival time schema: type: integer default: 900 minimum: 0 - name: requireBikeTransport in: query required: false schema: type: boolean default: false description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow bike carriage. - name: requireCarTransport in: query required: false schema: type: boolean default: false description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow car carriage. - name: maxPreTransitTime in: query required: false description: | Optional. Default is 15min which is `900`. Maximum time in seconds for the first street leg. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. schema: type: integer default: 900 minimum: 0 - name: maxPostTransitTime in: query required: false description: | Optional. Default is 15min which is `900`. Maximum time in seconds for the last street leg. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. schema: type: integer default: 900 minimum: 0 - name: maxDirectTime in: query required: false description: | Optional. Default is 30min which is `1800`. Maximum time in seconds for direct connections. Is limited by server config variable `street_routing_max_direct_seconds`. schema: type: integer default: 1800 minimum: 0 - name: fastestDirectFactor in: query required: false description: | Optional. Experimental. Default is `1.0`. Factor with which the duration of the fastest direct non-public-transit connection is multiplied. Values > 1.0 allow transit connections that are slower than the fastest direct non-public-transit connection to be found. schema: type: number default: 1.0 minimum: 0 - name: timeout in: query required: false description: Optional. Query timeout in seconds. schema: type: integer minimum: 0 - name: passengers in: query required: false description: Optional. Experimental. Number of passengers (e.g. for ODM or price calculation) schema: type: integer minimum: 1 - name: luggage in: query required: false description: | Optional. Experimental. Number of luggage pieces; base unit: airline cabin luggage (e.g. for ODM or price calculation) schema: type: integer minimum: 1 - name: slowDirect in: query required: false description: Optional. Experimental. Adds overtaken direct public transit connections. schema: type: boolean default: false - name: fastestSlowDirectFactor in: query required: false description: | Optional. Factor with which the duration of the fastest slowDirect connection is multiplied. Values > 1.0 allow connections that are slower than the fastest direct transit connection to be found. Values < 1.0 will return all slowDirect connections. schema: type: number default: 3.0 minimum: 0 - name: withFares in: query required: false description: Optional. Experimental. If set to true, the response will contain fare information. schema: type: boolean default: false - name: withScheduledSkippedStops in: query required: false description: Optional. Include intermediate stops where passengers can not alight/board according to schedule. schema: type: boolean default: false - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false - name: algorithm in: query required: false description: algorithm to use schema: type: string enum: - RAPTOR - PONG - TB default: PONG responses: '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: routing result content: application/json: schema: type: object required: - requestParameters - debugOutput - from - to - direct - itineraries - previousPageCursor - nextPageCursor properties: requestParameters: description: "the routing query" type: object additionalProperties: type: string debugOutput: description: "debug statistics" type: object additionalProperties: type: integer from: $ref: '#/components/schemas/Place' to: $ref: '#/components/schemas/Place' direct: description: | Direct trips by `WALK`, `BIKE`, `CAR`, etc. without time-dependency. The starting time (`arriveBy=false`) / arrival time (`arriveBy=true`) is always the queried `time` parameter (set to \"now\" if not set). But all `direct` connections are meant to be independent of absolute times. type: array items: $ref: '#/components/schemas/Itinerary' itineraries: description: list of itineraries type: array items: $ref: '#/components/schemas/Itinerary' previousPageCursor: description: | Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page. The previous page is a set of itineraries departing BEFORE the first itinerary in the result for a depart after search. When using the default sort order the previous set of itineraries is inserted before the current result. type: string nextPageCursor: description: | Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page. The next page is a set of itineraries departing AFTER the last itinerary in this result. type: string /api/v1/one-to-many: get: tags: - routing summary: | Street routing from one to many places or many to one. The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the query. operationId: oneToMany parameters: - name: one in: query required: true description: geo location as latitude;longitude schema: type: string - name: many in: query required: true description: | geo locations as latitude;longitude,latitude;longitude,... The number of accepted locations is limited by server config variable `onetomany_max_many`. schema: type: array items: type: string explode: false - name: mode in: query required: true description: | routing profile to use (currently supported: \`WALK\`, \`BIKE\`, \`CAR\`) schema: $ref: '#/components/schemas/Mode' - name: max in: query required: true description: maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`. schema: type: number - name: maxMatchingDistance in: query required: true description: maximum matching distance in meters to match geo coordinates to the street network schema: type: number - name: elevationCosts in: query required: false description: | Optional. Default is `NONE`. Set an elevation cost profile, to penalize routes with incline. - `NONE`: No additional costs for elevations. This is the default behavior - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. Elevation cost profiles are currently used by following street modes: - `BIKE` schema: $ref: '#/components/schemas/ElevationCosts' default: NONE - name: arriveBy in: query required: true description: | true = many to one false = one to many schema: type: boolean - name: withDistance in: query required: false description: | Optional. Default is `false`. If true, the response includes the distance in meters for each path. This requires path reconstruction and is slower than duration-only queries. schema: type: boolean default: false responses: '200': description: | A list of durations. If no path was found, the object is empty. content: application/json: schema: type: array items: $ref: '#/components/schemas/Duration' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' post: tags: - routing summary: | Street routing from one to many places or many to one. The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the request body. operationId: oneToManyPost requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OneToManyParams' responses: '200': description: | A list of durations. If no path was found, the object is empty. content: application/json: schema: type: array items: $ref: '#/components/schemas/Duration' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' /api/experimental/one-to-many-intermodal: get: tags: - routing summary: | One to many routing Computes the minimal duration from one place to many or vice versa. The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the query. operationId: oneToManyIntermodal parameters: - name: one in: query required: true description: geo location as latitude;longitude schema: type: string - name: many in: query required: true description: | geo locations as latitude;longitude,latitude;longitude,... The number of accepted locations is limited by server config variable `onetomany_max_many`. schema: type: array items: type: string explode: false - name: time in: query required: false description: | Optional. Defaults to the current time. Departure time ($arriveBy=false) / arrival date ($arriveBy=true), schema: type: string format: date-time - name: maxTravelTime in: query required: false description: | The maximum travel time in minutes. If not provided, the routing uses the value hardcoded in the server which is usually quite high. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the least transfers) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. schema: type: integer - name: maxMatchingDistance in: query required: false description: maximum matching distance in meters to match geo coordinates to the street network schema: type: number default: 25 - name: arriveBy in: query required: false description: | Optional. Defaults to false, i.e. one to many search true = many to one false = one to many schema: type: boolean default: false - name: maxTransfers in: query required: false description: | The maximum number of allowed transfers (i.e. interchanges between transit legs, pre- and postTransit do not count as transfers). `maxTransfers=0` searches for direct transit connections without any transfers. If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), send an empty `transitModes` parameter instead. If not provided, the routing uses the server-side default value which is hardcoded and very high to cover all use cases. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the fastest) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. schema: type: integer - name: minTransferTime in: query required: false description: | Optional. Default is 0 minutes. Minimum transfer time for each transfer in minutes. schema: type: integer default: 0 - name: additionalTransferTime in: query required: false description: | Optional. Default is 0 minutes. Additional transfer time reserved for each transfer in minutes. schema: type: integer default: 0 - name: transferTimeFactor in: query required: false description: | Optional. Default is 1.0 Factor to multiply minimum required transfer times with. Values smaller than 1.0 are not supported. schema: type: number default: 1.0 - name: useRoutedTransfers in: query required: false description: | Optional. Default is `false`. Whether to use transfers routed on OpenStreetMap data. schema: type: boolean default: false - name: pedestrianProfile in: query required: false description: | Optional. Default is `FOOT`. Accessibility profile to use for pedestrian routing in transfers between transit connections and the first and last mile respectively. schema: $ref: "#/components/schemas/PedestrianProfile" default: FOOT - name: pedestrianSpeed in: query required: false description: | Optional Average speed for pedestrian routing. schema: $ref: "#/components/schemas/PedestrianSpeed" - name: cyclingSpeed in: query required: false description: | Optional Average speed for bike routing. schema: $ref: "#/components/schemas/CyclingSpeed" - name: elevationCosts in: query required: false description: | Optional. Default is `NONE`. Set an elevation cost profile, to penalize routes with incline. - `NONE`: No additional costs for elevations. This is the default behavior - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. The profile is used for routing on both the first and last mile. Elevation cost profiles are currently used by following street modes: - `BIKE` schema: $ref: "#/components/schemas/ElevationCosts" default: NONE - name: transitModes in: query required: false description: | Optional. Default is `TRANSIT` which allows all transit modes (no restriction). Allowed modes for the transit part. If empty, no transit connections will be computed. For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. schema: type: array items: $ref: "#/components/schemas/Mode" default: - TRANSIT explode: false - name: preTransitModes in: query required: false description: | Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). A list of modes that are allowed to be used for the first mile, i.e. from the coordinates to the first transit stop. Example: `WALK,BIKE_SHARING`. schema: type: array items: $ref: "#/components/schemas/Mode" default: - WALK explode: false - name: postTransitModes in: query required: false description: | Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). A list of modes that are allowed to be used for the last mile, i.e. from the last transit stop to the target coordinates. Example: `WALK,BIKE_SHARING`. schema: type: array items: $ref: "#/components/schemas/Mode" default: - WALK explode: false - name: directMode in: query required: false description: | Default is `WALK` which will compute walking routes as direct connections. Mode used for direction connections from start to destination without using transit. Currently supported non-transit modes: \`WALK\`, \`BIKE\`, \`CAR\` schema: $ref: "#/components/schemas/Mode" default: WALK - name: maxPreTransitTime in: query required: false description: | Optional. Default is 15min which is `900`. Maximum time in seconds for the first street leg. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. schema: type: integer default: 900 minimum: 0 - name: maxPostTransitTime in: query required: false description: | Optional. Default is 15min which is `900`. Maximum time in seconds for the last street leg. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. schema: type: integer default: 900 minimum: 0 - name: maxDirectTime in: query required: false description: | Optional. Default is 30min which is `1800`. Maximum time in seconds for direct connections. If a value smaller than either `maxPreTransitTime` or `maxPostTransitTime` is used, their maximum is set instead. Is limited by server config variable `street_routing_max_direct_seconds`. schema: type: integer default: 1800 minimum: 0 - name: withDistance in: query required: false description: | Optional. Default is `false`. If true, the response includes the distance in meters for each path. This requires path reconstruction and is slower than duration-only queries. `withDistance` is currently limited to street routing. schema: type: boolean default: false - name: requireBikeTransport in: query required: false description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow bike carriage. schema: type: boolean default: false - name: requireCarTransport in: query required: false description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow car carriage. schema: type: boolean default: false responses: "200": description: | A list of durations. If no path was found, the object is empty. content: application/json: schema: $ref: "#/components/schemas/OneToManyIntermodalResponse" "400": description: Bad Request content: application/json: schema: $ref: "#/components/schemas/Error" "422": description: Unprocessable Entity content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal Server Error content: application/json: schema: $ref: "#/components/schemas/Error" post: tags: - routing summary: | One to many routing Computes the minimal duration from one place to many or vice versa. The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the request body. operationId: oneToManyIntermodalPost requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/OneToManyIntermodalParams" responses: "200": description: | A list of durations. If no path was found, the object is empty. content: application/json: schema: $ref: "#/components/schemas/OneToManyIntermodalResponse" "400": description: Bad Request content: application/json: schema: $ref: "#/components/schemas/Error" "422": description: Unprocessable Entity content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal Server Error content: application/json: schema: $ref: "#/components/schemas/Error" /api/v1/one-to-all: get: tags: - routing summary: | Computes all reachable locations from a given stop within a set duration. Each result entry will contain the fastest travel duration and the number of connections used. operationId: oneToAll parameters: - name: one in: query required: true description: | \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id schema: type: string - name: time in: query required: false description: | Optional. Defaults to the current time. Departure time ($arriveBy=false) / arrival date ($arriveBy=true), schema: type: string format: date-time - name: maxTravelTime in: query required: true description: The maximum travel time in minutes. Defaults to 90. The limit may be increased by the server administrator using `onetoall_max_travel_minutes` option in `config.yml`. See documentation for details. schema: type: integer - name: arriveBy in: query required: false description: | true = all to one, false = one to all schema: type: boolean default: false - name: maxTransfers in: query required: false description: | The maximum number of allowed transfers (i.e. interchanges between transit legs, pre- and postTransit do not count as transfers). `maxTransfers=0` searches for direct transit connections without any transfers. If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), send an empty `transitModes` parameter instead. If not provided, the routing uses the server-side default value which is hardcoded and very high to cover all use cases. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the fastest) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. In plan endpoints before v3, the behavior is off by one, i.e. `maxTransfers=0` only returns non-transit connections. schema: type: integer - name: minTransferTime in: query required: false description: | Optional. Default is 0 minutes. Minimum transfer time for each transfer in minutes. schema: type: integer default: 0 - name: additionalTransferTime in: query required: false description: | Optional. Default is 0 minutes. Additional transfer time reserved for each transfer in minutes. schema: type: integer default: 0 - name: transferTimeFactor in: query required: false description: | Optional. Default is 1.0 Factor to multiply minimum required transfer times with. Values smaller than 1.0 are not supported. schema: type: number default: 1.0 - name: maxMatchingDistance in: query required: false description: | Optional. Default is 25 meters. Maximum matching distance in meters to match geo coordinates to the street network. schema: type: number default: 25 - name: useRoutedTransfers in: query required: false description: | Optional. Default is `false`. Whether to use transfers routed on OpenStreetMap data. schema: type: boolean default: false - name: pedestrianProfile in: query required: false description: | Optional. Default is `FOOT`. Accessibility profile to use for pedestrian routing in transfers between transit connections and the first and last mile respectively. schema: $ref: '#/components/schemas/PedestrianProfile' default: FOOT - name: pedestrianSpeed in: query required: false description: | Optional Average speed for pedestrian routing. schema: $ref: '#/components/schemas/PedestrianSpeed' - name: cyclingSpeed in: query required: false description: | Optional Average speed for bike routing. schema: $ref: '#/components/schemas/CyclingSpeed' - name: elevationCosts in: query required: false description: | Optional. Default is `NONE`. Set an elevation cost profile, to penalize routes with incline. - `NONE`: No additional costs for elevations. This is the default behavior - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. The profile is used for routing on both the first and last mile. Elevation cost profiles are currently used by following street modes: - `BIKE` schema: $ref: '#/components/schemas/ElevationCosts' default: NONE - name: transitModes in: query required: false description: | Optional. Default is `TRANSIT` which allows all transit modes (no restriction). Allowed modes for the transit part. If empty, no transit connections will be computed. For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. schema: default: - TRANSIT type: array items: $ref: '#/components/schemas/Mode' explode: false - name: preTransitModes in: query required: false description: | Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set: - `arriveBy=true`: Currently not used - `arriveBy=false`: Only applies if the `one` place is a coordinate (not a transit stop). A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`. schema: default: - WALK type: array items: $ref: '#/components/schemas/Mode' explode: false - name: postTransitModes in: query required: false description: | Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set: - `arriveBy=true`: Only applies if the `one` place is a coordinate (not a transit stop). - `arriveBy=false`: Currently not used A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`. schema: default: - WALK type: array items: $ref: '#/components/schemas/Mode' explode: false - name: requireBikeTransport in: query required: false schema: type: boolean default: false description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow bike carriage. - name: requireCarTransport in: query required: false schema: type: boolean default: false description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow car carriage. - name: maxPreTransitTime in: query required: false description: | Optional. Default is 15min which is `900`. - `arriveBy=true`: Currently not used - `arriveBy=false`: Maximum time in seconds for the street leg at `one` location. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. schema: type: integer default: 900 minimum: 0 - name: maxPostTransitTime in: query required: false description: | Optional. Default is 15min which is `900`. - `arriveBy=true`: Maximum time in seconds for the street leg at `one` location. - `arriveBy=false`: Currently not used Is limited by server config variable `street_routing_max_prepost_transit_seconds`. schema: type: integer default: 900 minimum: 0 responses: '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: | The starting position and a list of all reachable stops If no paths are found, the reachable list is empty. content: application/json: schema: $ref: '#/components/schemas/Reachable' /api/v1/reverse-geocode: get: tags: - geocode summary: Translate coordinates to the closest address(es)/places/stops. operationId: reverseGeocode parameters: - name: place in: query required: true description: latitude, longitude in degrees schema: type: string - name: type in: query required: false description: | Optional. Default is all types. Only return results of the given type. For example, this can be used to allow only `ADDRESS` and `STOP` results. schema: $ref: '#/components/schemas/LocationType' - name: numResults in: query required: false description: | Optional. Number of results to return. If omitted, 5 results are returned by default. Must be <= server config variable `reverse_geocode_max_results`. schema: type: integer minimum: 1 responses: '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: A list of guesses to resolve the coordinates to a location content: application/json: schema: type: array items: $ref: '#/components/schemas/Match' /api/v1/geocode: get: tags: - geocode summary: Autocompletion & geocoding that resolves user input addresses including coordinates operationId: geocode parameters: - name: text in: query required: true description: the (potentially partially typed) address to resolve schema: type: string - name: language in: query required: false description: | language tags as used in OpenStreetMap (usually ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false - name: type in: query required: false description: | Optional. Default is all types. Only return results of the given types. For example, this can be used to allow only `ADDRESS` and `STOP` results. schema: $ref: '#/components/schemas/LocationType' - name: mode in: query required: false description: | Optional. Filter stops by available transport modes. Defaults to applying no filter. schema: type: array items: $ref: '#/components/schemas/Mode' explode: false - name: place in: query required: false description: | Optional. Used for biasing results towards the coordinate. Format: latitude,longitude in degrees schema: type: string - name: placeBias in: query required: false description: | Optional. Used for biasing results towards the coordinate. Higher number = higher bias. schema: type: number default: 1 - name: numResults in: query required: false description: | Optional. Number of suggestions to return. If omitted, 10 suggestions are returned by default. Must be <= server config variable `geocode_max_suggestions`. schema: type: integer minimum: 1 responses: '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: A list of guesses to resolve the text to a location content: application/json: schema: type: array items: $ref: '#/components/schemas/Match' /api/v5/trip: get: tags: - timetable summary: Get a trip as itinerary operationId: trip parameters: - name: tripId in: query schema: type: string required: true description: trip identifier (e.g. from an itinerary leg or stop event) - name: withScheduledSkippedStops in: query required: false description: Optional. Include intermediate stops where passengers can not alight/board according to schedule. schema: type: boolean default: false - name: detailedLegs in: query required: false description: | Controls if `legGeometry` is returned for transit legs. The default value is `true`. schema: type: boolean default: true - name: joinInterlinedLegs in: query description: | Optional. Default is `true`. Controls if a trip with stay-seated transfers is returned: - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.). Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`. - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops schema: type: boolean default: true - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false responses: '200': description: the requested trip as itinerary content: application/json: schema: $ref: '#/components/schemas/Itinerary' '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' /api/v5/stoptimes: get: tags: - timetable summary: Get the next N departures or arrivals of a stop sorted by time operationId: stoptimes parameters: - name: stopId in: query schema: type: string required: false description: stop id of the stop to retrieve departures/arrivals for - name: center in: query schema: type: string required: false description: | Anchor coordinate. Format: latitude,longitude pair. Used as fallback when "stopId" is missing or can't be found. If both are provided and "stopId" resolves, "stopId" is used. If "stopId" does not resolve, "center" is used instead. "radius" is required when querying by "center" (i.e. without a valid "stopId"). - name: time in: query required: false description: | Optional. Defaults to the current time. schema: type: string format: date-time - name: arriveBy in: query required: false schema: type: boolean default: false description: | Optional. Default is `false`. - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time - `arriveBy=false`: the parameters `date` and `time` refer to the departure time - name: direction in: query required: false schema: type: string enum: - EARLIER - LATER description: | This parameter will be ignored in case `pageCursor` is set. Optional. Default is - `LATER` for `arriveBy=false` - `EARLIER` for `arriveBy=true` The response will contain the next `n` arrivals / departures in case `EARLIER` is selected and the previous `n` arrivals / departures if `LATER` is selected. - name: window in: query required: false description: | Optional. Window in seconds around `time`. Limiting the response to those that are at most `window` seconds aways in time. If both `n` and `window` are set, it uses whichever returns more. schema: type: integer minimum: 0 - name: mode in: query schema: type: array items: $ref: '#/components/schemas/Mode' default: - TRANSIT explode: false description: | Optional. Default is all transit modes. Only return arrivals/departures of the given modes. - name: n in: query schema: type: integer required: false description: | Minimum number of events to return. If both `n` and `window` are provided, the API uses whichever returns more events. - name: radius in: query schema: type: integer required: false description: | Optional. Radius in meters. Default is that only stop times of the parent of the stop itself and all stops with the same name (+ their child stops) are returned. If set, all stops at parent stations and their child stops in the specified radius are returned. - name: exactRadius in: query schema: type: boolean default: false required: false description: | Optional. Default is `false`. If set to `true`, only stations that are phyiscally in the radius are considered. If set to `false`, additionally to the stations in the radius, equivalences with the same name and children are considered. - name: fetchStops in: query schema: type: boolean required: false description: | Experimental. Expect unannounced breaking changes (without version bumps). Optional. Default is `false`. If set to `true`, the following stops are returned for departures and the previous stops are returned for arrivals. - name: pageCursor in: query required: false description: | Use the cursor to go to the next "page" of stop times. Copy the cursor from the last response and keep the original request as is. This will enable you to search for stop times in the next or previous time-window. schema: type: string - name: withScheduledSkippedStops in: query required: false description: Optional. Include stoptimes where passengers can not alight/board according to schedule. schema: type: boolean default: false - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false - name: withAlerts in: query required: false description: Optional. Default is `true`. If set to `false`, alerts are omitted in the metadata of place for all stopTimes. schema: type: boolean default: true responses: '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: A list of departures/arrivals content: application/json: schema: type: object required: - stopTimes - place - previousPageCursor - nextPageCursor properties: stopTimes: description: list of stop times type: array items: $ref: '#/components/schemas/StopTime' place: description: metadata of the requested stop $ref: '#/components/schemas/Place' previousPageCursor: description: | Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page. The previous page is a set of stop times BEFORE the first stop time in the result. type: string nextPageCursor: description: | Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page. The next page is a set of stop times AFTER the last stop time in this result. type: string /api/v5/map/trips: get: tags: - map operationId: trips summary: | Given a area frame (box defined by top right and bottom left corner) and a time frame, it returns all trips and their respective shapes that operate in this area + time frame. Trips are filtered by zoom level. On low zoom levels, only long distance trains will be shown while on high zoom levels, also metros, buses and trams will be returned. parameters: - name: zoom in: query required: true description: current zoom level schema: type: number - name: min in: query required: true description: latitude,longitude pair of the lower right coordinate schema: type: string - name: max in: query required: true description: latitude,longitude pair of the upper left coordinate schema: type: string - name: startTime in: query required: true description: start of the time window schema: type: string format: date-time - name: endTime in: query required: true description: end if the time window schema: type: string format: date-time - name: precision in: query required: false description: "precision of returned polylines. Recommended to set based on zoom: `zoom >= 11 ? 5 : zoom >= 8 ? 4 : zoom >= 5 ? 3 : 2`" schema: type: number minimum: 0 maximum: 6 default: 5 - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false responses: '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: a list of trips content: application/json: schema: type: array items: $ref: '#/components/schemas/TripSegment' /api/v1/map/initial: get: tags: - map operationId: initial summary: initial location to view the map at after loading based on where public transport should be visible responses: '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: latitude, longitude, zoom level to set the map to, and routing options configuration and limits content: application/json: schema: type: object required: - lat - lon - zoom - serverConfig properties: lat: description: latitude type: number lon: description: longitude type: number zoom: description: zoom level type: number serverConfig: $ref: '#/components/schemas/ServerConfig' /api/v1/map/stops: get: tags: - map summary: Get all stops for a map section operationId: stops parameters: - name: min in: query required: true description: latitude,longitude pair of the lower right coordinate schema: type: string - name: max in: query required: true description: latitude,longitude pair of the upper left coordinate schema: type: string - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false responses: '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: array of stop places in the selected map section content: application/json: schema: type: array items: $ref: '#/components/schemas/Place' /api/v1/map/levels: get: tags: - map summary: Get all available levels for a map section operationId: levels parameters: - name: min in: query required: true description: latitude,longitude pair of the lower right coordinate schema: type: string - name: max in: query required: true description: latitude,longitude pair of the upper left coordinate schema: type: string responses: '500': description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: array of available levels content: application/json: schema: type: array items: type: number /api/experimental/map/routes: get: tags: - map operationId: routes summary: | Given an area frame (box defined by the top-right and bottom-left corners), it returns all routes and their respective shapes that operate within this area. Routes are filtered by zoom level. On low zoom levels, only long distance trains will be shown while on high zoom levels, also metros, buses and trams will be returned. parameters: - name: zoom in: query required: true description: current zoom level schema: type: number - name: min in: query required: true description: latitude,longitude pair of the lower right coordinate schema: type: string - name: max in: query required: true description: latitude,longitude pair of the upper left coordinate schema: type: string - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false responses: '422': description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/Error' '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Server Error content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: a list of routes in the area content: application/json: schema: type: object required: - routes - polylines - stops - zoomFiltered properties: routes: type: array items: $ref: '#/components/schemas/RouteInfo' polylines: type: array items: $ref: '#/components/schemas/RoutePolyline' stops: type: array items: $ref: '#/components/schemas/Place' zoomFiltered: type: boolean description: | Indicates whether some routes were filtered out due to the zoom level. /api/experimental/map/route-details: get: tags: - map operationId: routeDetails summary: | Returns the full data for a single route, including all stops and polyline segments. parameters: - name: routeIdx in: query required: true description: Internal route index schema: type: integer - name: language in: query required: false description: | language tags as used in OpenStreetMap / GTFS (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) schema: type: array items: type: string explode: false responses: "422": description: Unprocessable Entity content: application/json: schema: $ref: "#/components/schemas/Error" "400": description: Bad Request content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Not Found content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Server Error content: application/json: schema: $ref: "#/components/schemas/Error" "200": description: full data for a single route content: application/json: schema: type: object required: - routes - polylines - stops - zoomFiltered properties: routes: type: array items: $ref: "#/components/schemas/RouteInfo" polylines: type: array items: $ref: "#/components/schemas/RoutePolyline" stops: type: array items: $ref: "#/components/schemas/Place" zoomFiltered: type: boolean description: Always false for this endpoint. /api/v1/rentals: get: tags: - map summary: | Get a list of rental providers or all rental stations and vehicles for a map section or provider operationId: rentals description: | Various options to filter the providers, stations and vehicles are available. If none of these filters are provided, a list of all available rental providers is returned without any station, vehicle or zone data. At least one of the following filters must be provided to retrieve station, vehicle and zone data: - A bounding box defined by the `min` and `max` parameters - A circle defined by the `point` and `radius` parameters - A list of provider groups via the `providerGroups` parameter - A list of providers via the `providers` parameter Only data that matches all the provided filters is returned. Provide the `withProviders=false` parameter to retrieve only provider groups if detailed feed information is not required. parameters: - name: min in: query required: false description: latitude,longitude pair of the lower right coordinate schema: type: string - name: max in: query required: false description: latitude,longitude pair of the upper left coordinate schema: type: string - name: point in: query required: false description: | \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (ignored, for compatibility reasons) OR stop id schema: type: string - name: radius in: query schema: type: integer required: false description: | Radius around `point` in meters. - name: providerGroups in: query required: false description: | A list of rental provider groups to return. If both `providerGroups` and `providers` are empty/not specified, all providers in the map section are returned. schema: type: array items: type: string explode: false - name: providers in: query required: false description: | A list of rental providers to return. If both `providerGroups` and `providers` are empty/not specified, all providers in the map section are returned. schema: type: array items: type: string explode: false - name: withProviders in: query required: false description: | Optional. Include providers in output. If false, only provider groups are returned. Also affects the providers list for each provider group. schema: type: boolean default: true - name: withStations in: query required: false description: Optional. Include stations in output (requires at least min+max or providers filter). schema: type: boolean default: true - name: withVehicles in: query required: false description: Optional. Include free-floating vehicles in output (requires at least min+max or providers filter). schema: type: boolean default: true - name: withZones in: query required: false description: Optional. Include geofencing zones in output (requires at least min+max or providers filter). schema: type: boolean default: true responses: '400': description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' '200': description: Rentals in the map section or for the given providers content: application/json: schema: type: object required: - providerGroups - providers - stations - vehicles - zones properties: providerGroups: type: array items: $ref: '#/components/schemas/RentalProviderGroup' providers: type: array items: $ref: '#/components/schemas/RentalProvider' stations: type: array items: $ref: '#/components/schemas/RentalStation' vehicles: type: array items: $ref: '#/components/schemas/RentalVehicle' zones: type: array items: $ref: '#/components/schemas/RentalZone' /api/debug/transfers: get: tags: - debug summary: Prints all transfers of a timetable location (track, bus stop, etc.) operationId: transfers parameters: - name: id in: query required: true description: location id schema: type: string responses: '200': description: list of outgoing transfers of this location content: application/json: schema: type: object required: - place - root - equivalences - hasFootTransfers - hasWheelchairTransfers - hasCarTransfers - transfers properties: place: $ref: '#/components/schemas/Place' root: $ref: '#/components/schemas/Place' equivalences: type: array items: $ref: '#/components/schemas/Place' hasFootTransfers: type: boolean description: true if the server has foot transfers computed hasWheelchairTransfers: type: boolean description: true if the server has wheelchair transfers computed hasCarTransfers: type: boolean description: true if the server has car transfers computed transfers: description: all outgoing transfers of this location type: array items: $ref: '#/components/schemas/Transfer' components: schemas: AlertCause: description: Cause of this alert. type: string enum: - UNKNOWN_CAUSE - OTHER_CAUSE - TECHNICAL_PROBLEM - STRIKE - DEMONSTRATION - ACCIDENT - HOLIDAY - WEATHER - MAINTENANCE - CONSTRUCTION - POLICE_ACTIVITY - MEDICAL_EMERGENCY AlertEffect: description: The effect of this problem on the affected entity. type: string enum: - NO_SERVICE - REDUCED_SERVICE - SIGNIFICANT_DELAYS - DETOUR - ADDITIONAL_SERVICE - MODIFIED_SERVICE - OTHER_EFFECT - UNKNOWN_EFFECT - STOP_MOVED - NO_EFFECT - ACCESSIBILITY_ISSUE AlertSeverityLevel: description: The severity of the alert. type: string enum: - UNKNOWN_SEVERITY - INFO - WARNING - SEVERE TimeRange: description: | A time interval. The interval is considered active at time t if t is greater than or equal to the start time and less than the end time. type: object required: - start - end properties: start: description: | If missing, the interval starts at minus infinity. If a TimeRange is provided, either start or end must be provided - both fields cannot be empty. type: string format: date-time end: description: | If missing, the interval ends at plus infinity. If a TimeRange is provided, either start or end must be provided - both fields cannot be empty. type: string format: date-time Alert: description: An alert, indicating some sort of incident in the public transit network. type: object required: - headerText - descriptionText properties: code: type: string description: Attribute or notice code (e.g. for HRDF or NeTEx) communicationPeriod: description: | Time when the alert should be shown to the user. If missing, the alert will be shown as long as it appears in the feed. If multiple ranges are given, the alert will be shown during all of them. type: array items: $ref: '#/components/schemas/TimeRange' impactPeriod: description: Time when the services are affected by the disruption mentioned in the alert. type: array items: $ref: '#/components/schemas/TimeRange' cause: $ref: '#/components/schemas/AlertCause' causeDetail: type: string description: | Description of the cause of the alert that allows for agency-specific language; more specific than the Cause. effect: $ref: '#/components/schemas/AlertEffect' effectDetail: type: string description: | Description of the effect of the alert that allows for agency-specific language; more specific than the Effect. url: type: string description: The URL which provides additional information about the alert. headerText: type: string description: | Header for the alert. This plain-text string will be highlighted, for example in boldface. descriptionText: type: string description: | Description for the alert. This plain-text string will be formatted as the body of the alert (or shown on an explicit "expand" request by the user). The information in the description should add to the information of the header. ttsHeaderText: type: string description: | Text containing the alert's header to be used for text-to-speech implementations. This field is the text-to-speech version of header_text. It should contain the same information as headerText but formatted such that it can read as text-to-speech (for example, abbreviations removed, numbers spelled out, etc.) ttsDescriptionText: type: string description: | Text containing a description for the alert to be used for text-to-speech implementations. This field is the text-to-speech version of description_text. It should contain the same information as description_text but formatted such that it can be read as text-to-speech (for example, abbreviations removed, numbers spelled out, etc.) severityLevel: description: Severity of the alert. $ref: '#/components/schemas/AlertSeverityLevel' imageUrl: description: String containing an URL linking to an image. type: string imageMediaType: description: | IANA media type as to specify the type of image to be displayed. The type must start with "image/" type: string imageAlternativeText: description: | Text describing the appearance of the linked image in the image field (e.g., in case the image can't be displayed or the user can't see the image for accessibility reasons). See the HTML spec for alt image text. type: string Duration: description: Object containing duration if a path was found or none if no path was found type: object properties: duration: type: number description: duration in seconds if a path was found, otherwise missing minimum: 0.0 distance: type: number description: distance in meters if a path was found and distance computation was requested, otherwise missing minimum: 0.0 ParetoSetEntry: description: Object containing a single element of a ParetoSet type: object required: - duration - transfers properties: duration: type: number description: | duration in seconds for the the best solution using `transfer` transfers Notice that the resolution is currently in minutes, because of implementation details minimum: 0.0 transfers: description: | The minimal number of transfers required to arrive within `duration` seconds transfers=0: Direct transit connecion without any transfers transfers=1: Transit connection with 1 transfer type: integer minimum: 0 ParetoSet: description: Pareto set of optimal transit solutions type: array items: $ref: '#/components/schemas/ParetoSetEntry' OneToManyIntermodalResponse: description: Object containing the optimal street and transit durations for One-to-Many routing type: object properties: street_durations: description: | Fastest durations for street routing The order of the items corresponds to the order of the `many` locations If no street routed connection is found, the corresponding `Duration` will be empty type: array items: $ref: '#/components/schemas/Duration' transit_durations: description: | Pareto optimal solutions The order of the items corresponds to the order of the `many` locations If no connection using transits is found, the corresponding `ParetoSet` will be empty type: array items: $ref: '#/components/schemas/ParetoSet' Area: description: Administrative area type: object required: - name - adminLevel - matched properties: name: type: string description: Name of the area adminLevel: type: number description: | [OpenStreetMap `admin_level`](https://wiki.openstreetmap.org/wiki/Key:admin_level) of the area matched: type: boolean description: Whether this area was matched by the input text unique: type: boolean description: | Set for the first area after the `default` area that distinguishes areas if the match is ambiguous regarding (`default` area + place name / street [+ house number]). default: type: boolean description: Whether this area should be displayed as default area (area with admin level closest 7) Token: description: Matched token range (from index, length) type: array minItems: 2 maxItems: 2 items: type: number LocationType: description: location type type: string enum: - ADDRESS - PLACE - STOP Mode: description: | # Street modes - `WALK` - `BIKE` - `RENTAL` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs. - `CAR` - `CAR_PARKING` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs. - `CAR_DROPOFF` Experimental. Expect unannounced breaking changes (without version bumps) for all perameters and returned structs. - `ODM` on-demand taxis from the Prima+ÖV Project - `RIDE_SHARING` ride sharing from the Prima+ÖV Project - `FLEX` flexible transports # Transit modes - `TRANSIT`: translates to `TRAM,FERRY,AIRPLANE,BUS,COACH,RAIL,ODM,FUNICULAR,AERIAL_LIFT,OTHER` - `TRAM`: trams - `SUBWAY`: subway trains (Paris Metro, London Underground, but also NYC Subway, Hamburger Hochbahn, and other non-underground services) - `FERRY`: ferries - `AIRPLANE`: airline flights - `BUS`: short distance buses (does not include `COACH`) - `COACH`: long distance buses (does not include `BUS`) - `RAIL`: translates to `HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN,SUBWAY` - `HIGHSPEED_RAIL`: long distance high speed trains (e.g. TGV) - `LONG_DISTANCE`: long distance inter city trains - `NIGHT_RAIL`: long distance night trains - `REGIONAL_FAST_RAIL`: deprecated, `REGIONAL_RAIL` will be used - `REGIONAL_RAIL`: regional train - `SUBURBAN`: suburban trains (e.g. S-Bahn, RER, Elizabeth Line, ...) - `ODM`: demand responsive transport - `FUNICULAR`: Funicular. Any rail system designed for steep inclines. - `AERIAL_LIFT`: Aerial lift, suspended cable car (e.g., gondola lift, aerial tramway). Cable transport where cabins, cars, gondolas or open chairs are suspended by means of one or more cables. - `AREAL_LIFT`: deprecated - `METRO`: deprecated - `CABLE_CAR`: deprecated type: string enum: # === Street === - WALK - BIKE - RENTAL - CAR - CAR_PARKING - CAR_DROPOFF - ODM # Transit | Street - RIDE_SHARING # Transit | Street - FLEX - DEBUG_BUS_ROUTE - DEBUG_RAILWAY_ROUTE - DEBUG_FERRY_ROUTE # === Transit === - TRANSIT - TRAM - SUBWAY - FERRY - AIRPLANE - BUS - COACH - RAIL - HIGHSPEED_RAIL - LONG_DISTANCE - NIGHT_RAIL - REGIONAL_FAST_RAIL - REGIONAL_RAIL - SUBURBAN - FUNICULAR - AERIAL_LIFT - OTHER - AREAL_LIFT - METRO - CABLE_CAR Match: description: GeoCoding match type: object required: - type - name - id - lat - lon - tokens - areas - score properties: type: $ref: '#/components/schemas/LocationType' category: description: | Experimental. Type categories might be adjusted. For OSM stop locations: the amenity type based on https://wiki.openstreetmap.org/wiki/OpenStreetMap_Carto/Symbols type: string tokens: description: list of non-overlapping tokens that were matched type: array items: $ref: '#/components/schemas/Token' name: description: name of the location (transit stop / PoI / address) type: string id: description: unique ID of the location type: string lat: description: latitude type: number lon: description: longitude type: number level: description: | level according to OpenStreetMap (at the moment only for public transport) type: number street: description: street name type: string houseNumber: description: house number type: string country: description: ISO3166-1 country code from OpenStreetMap type: string zip: description: zip code type: string tz: description: timezone name (e.g. "Europe/Berlin") type: string areas: description: list of areas type: array items: $ref: '#/components/schemas/Area' score: description: score according to the internal scoring system (the scoring algorithm might change in the future) type: number modes: description: available transport modes for stops type: array items: $ref: "#/components/schemas/Mode" importance: description: importance of a stop, normalized from [0, 1] type: number ElevationCosts: description: | Different elevation cost profiles for street routing. Using a elevation cost profile will prefer routes with a smaller incline and smaller difference in elevation, even if the routed way is longer. - `NONE`: Ignore elevation data for routing. This is the default behavior - `LOW`: Add a low penalty for inclines. This will favor longer paths, if the elevation increase and incline are smaller. - `HIGH`: Add a high penalty for inclines. This will favor even longer paths, if the elevation increase and incline are smaller. type: string enum: - NONE - LOW - HIGH PedestrianProfile: description: Different accessibility profiles for pedestrians. type: string enum: - FOOT - WHEELCHAIR PedestrianSpeed: description: Average speed for pedestrian routing in meters per second type: number CyclingSpeed: description: Average speed for bike routing in meters per second type: number VertexType: type: string description: | - `NORMAL` - latitude / longitude coordinate or address - `BIKESHARE` - bike sharing station - `TRANSIT` - transit stop enum: - NORMAL - BIKESHARE - TRANSIT PickupDropoffType: type: string description: | - `NORMAL` - entry/exit is possible normally - `NOT_ALLOWED` - entry/exit is not allowed enum: - NORMAL - NOT_ALLOWED Place: type: object required: - name - lat - lon - level properties: name: description: name of the transit stop / PoI / address type: string stopId: description: The ID of the stop. This is often something that users don't care about. type: string parentId: description: If it's not a root stop, this field contains the `stopId` of the parent stop. type: string importance: description: The importance of the stop between 0-1. type: number lat: description: latitude type: number lon: description: longitude type: number level: description: level according to OpenStreetMap type: number tz: description: timezone name (e.g. "Europe/Berlin") type: string arrival: description: arrival time type: string format: date-time departure: description: departure time type: string format: date-time scheduledArrival: description: scheduled arrival time type: string format: date-time scheduledDeparture: description: scheduled departure time type: string format: date-time scheduledTrack: description: scheduled track from the static schedule timetable dataset type: string track: description: | The current track/platform information, updated with real-time updates if available. Can be missing if neither real-time updates nor the schedule timetable contains track information. type: string description: description: description of the location that provides more detailed information type: string vertexType: $ref: '#/components/schemas/VertexType' pickupType: description: Type of pickup. It could be disallowed due to schedule, skipped stops or cancellations. $ref: '#/components/schemas/PickupDropoffType' dropoffType: description: Type of dropoff. It could be disallowed due to schedule, skipped stops or cancellations. $ref: '#/components/schemas/PickupDropoffType' cancelled: description: Whether this stop is cancelled due to the realtime situation. type: boolean alerts: description: Alerts for this stop. type: array items: $ref: '#/components/schemas/Alert' flex: description: for `FLEX` transports, the flex location area or location group name type: string flexId: description: for `FLEX` transports, the flex location area ID or location group ID type: string flexStartPickupDropOffWindow: description: Time that on-demand service becomes available type: string format: date-time flexEndPickupDropOffWindow: description: Time that on-demand service ends type: string format: date-time modes: description: available transport modes for stops type: array items: $ref: "#/components/schemas/Mode" ReachablePlace: description: Place reachable by One-to-All type: object properties: place: $ref: "#/components/schemas/Place" description: Place reached by One-to-All duration: type: integer description: Total travel duration k: type: integer description: | k is the smallest number, for which a journey with the shortest duration and at most k-1 transfers exist. You can think of k as the number of connections used. In more detail: k=0: No connection, e.g. for the one location k=1: Direct connection k=2: Connection with 1 transfer Reachable: description: Object containing all reachable places by One-to-All search type: object properties: one: $ref: "#/components/schemas/Place" description: One location used in One-to-All search all: description: List of locations reachable by One-to-All type: array items: $ref: "#/components/schemas/ReachablePlace" StopTime: description: departure or arrival event at a stop type: object required: - place - mode - realTime - headsign - tripFrom - tripTo - agencyId - agencyName - agencyUrl - tripId - routeId - directionId - routeShortName - routeLongName - tripShortName - displayName - pickupDropoffType - cancelled - tripCancelled - source properties: place: $ref: '#/components/schemas/Place' description: information about the stop place and time mode: $ref: '#/components/schemas/Mode' description: Transport mode for this leg realTime: description: Whether there is real-time data about this leg type: boolean headsign: description: | The headsign of the bus or train being used. For non-transit legs, null type: string tripFrom: description: first stop of this trip $ref: '#/components/schemas/Place' tripTo: description: final stop of this trip $ref: '#/components/schemas/Place' agencyId: type: string agencyName: type: string agencyUrl: type: string routeId: type: string routeUrl: type: string directionId: type: string routeColor: type: string routeTextColor: type: string tripId: type: string routeType: type: integer routeShortName: type: string routeLongName: type: string tripShortName: type: string displayName: type: string previousStops: type: array description: | Experimental. Expect unannounced breaking changes (without version bumps). Stops on the trips before this stop. Returned only if `fetchStop` and `arriveBy` are `true`. items: $ref: "#/components/schemas/Place" nextStops: type: array description: | Experimental. Expect unannounced breaking changes (without version bumps). Stops on the trips after this stop. Returned only if `fetchStop` is `true` and `arriveBy` is `false`. items: $ref: "#/components/schemas/Place" pickupDropoffType: description: Type of pickup (for departures) or dropoff (for arrivals), may be disallowed either due to schedule, skipped stops or cancellations $ref: '#/components/schemas/PickupDropoffType' cancelled: description: Whether the departure/arrival is cancelled due to the realtime situation (either because the stop is skipped or because the entire trip is cancelled). type: boolean tripCancelled: description: Whether the entire trip is cancelled due to the realtime situation. type: boolean source: description: Filename and line number where this trip is from type: string TripInfo: description: trip id and name type: object required: - tripId properties: tripId: description: trip ID (dataset trip id prefixed with the dataset tag) type: string routeShortName: description: trip display name (api version < 4) type: string displayName: description: trip display name (api version >= 4) type: string TripSegment: description: trip segment between two stops to show a trip on a map type: object required: - trips - mode - distance - from - to - departure - arrival - scheduledArrival - scheduledDeparture - realTime - polyline properties: trips: type: array items: $ref: '#/components/schemas/TripInfo' routeColor: type: string mode: $ref: '#/components/schemas/Mode' description: Transport mode for this leg distance: type: number description: distance in meters from: $ref: '#/components/schemas/Place' to: $ref: '#/components/schemas/Place' departure: description: departure time type: string format: date-time arrival: description: arrival time type: string format: date-time scheduledDeparture: description: scheduled departure time type: string format: date-time scheduledArrival: description: scheduled arrival time type: string format: date-time realTime: description: Whether there is real-time data about this leg type: boolean polyline: description: Google polyline encoded coordinate sequence (with precision 5) where the trip travels on this segment. type: string Direction: type: string enum: - DEPART - HARD_LEFT - LEFT - SLIGHTLY_LEFT - CONTINUE - SLIGHTLY_RIGHT - RIGHT - HARD_RIGHT - CIRCLE_CLOCKWISE - CIRCLE_COUNTERCLOCKWISE - STAIRS - ELEVATOR - UTURN_LEFT - UTURN_RIGHT EncodedPolyline: type: object required: - points - precision - length properties: points: description: The encoded points of the polyline using the Google polyline encoding. type: string precision: description: | The precision of the returned polyline (7 for /v1, 6 for /v2) Be aware that with precision 7, coordinates with |longitude| > 107.37 are undefined/will overflow. type: integer length: description: The number of points in the string type: integer minimum: 0 StepInstruction: type: object required: - fromLevel - toLevel - polyline - relativeDirection - distance - streetName - exit - stayOn - area properties: relativeDirection: $ref: '#/components/schemas/Direction' distance: description: The distance in meters that this step takes. type: number fromLevel: description: level where this segment starts, based on OpenStreetMap data type: number toLevel: description: level where this segment starts, based on OpenStreetMap data type: number osmWay: description: OpenStreetMap way index type: integer polyline: $ref: '#/components/schemas/EncodedPolyline' streetName: description: The name of the street. type: string exit: description: | Not implemented! When exiting a highway or traffic circle, the exit name/number. type: string stayOn: description: | Not implemented! Indicates whether or not a street changes direction at an intersection. type: boolean area: description: | Not implemented! This step is on an open area, such as a plaza or train platform, and thus the directions should say something like "cross" type: boolean toll: description: Indicates that a fee must be paid by general traffic to use a road, road bridge or road tunnel. type: boolean accessRestriction: description: | Experimental. Indicates whether access to this part of the route is restricted. See: https://wiki.openstreetmap.org/wiki/Conditional_restrictions type: string elevationUp: type: integer description: incline in meters across this path segment elevationDown: type: integer description: decline in meters across this path segment RentalFormFactor: type: string enum: - BICYCLE - CARGO_BICYCLE - CAR - MOPED - SCOOTER_STANDING - SCOOTER_SEATED - OTHER RentalPropulsionType: type: string enum: - HUMAN - ELECTRIC_ASSIST - ELECTRIC - COMBUSTION - COMBUSTION_DIESEL - HYBRID - PLUG_IN_HYBRID - HYDROGEN_FUEL_CELL RentalReturnConstraint: type: string enum: - NONE - ANY_STATION - ROUNDTRIP_STATION Rental: description: Vehicle rental type: object required: - providerId - providerGroupId - systemId properties: providerId: type: string description: Rental provider ID providerGroupId: type: string description: Rental provider group ID systemId: type: string description: Vehicle share system ID systemName: type: string description: Vehicle share system name url: type: string description: URL of the vehicle share system color: type: string description: | Color associated with this provider, in hexadecimal RGB format (e.g. "#FF0000" for red). Can be empty. stationName: type: string description: Name of the station fromStationName: type: string description: Name of the station where the vehicle is picked up (empty for free floating vehicles) toStationName: type: string description: Name of the station where the vehicle is returned (empty for free floating vehicles) rentalUriAndroid: type: string description: Rental URI for Android (deep link to the specific station or vehicle) rentalUriIOS: type: string description: Rental URI for iOS (deep link to the specific station or vehicle) rentalUriWeb: type: string description: Rental URI for web (deep link to the specific station or vehicle) formFactor: $ref: '#/components/schemas/RentalFormFactor' propulsionType: $ref: '#/components/schemas/RentalPropulsionType' returnConstraint: $ref: '#/components/schemas/RentalReturnConstraint' MultiPolygon: type: array description: | A multi-polygon contains a number of polygons, each containing a number of rings, which are encoded as polylines (with precision 6). For each polygon, the first ring is the outer ring, all subsequent rings are inner rings (holes). items: # polygons type: array items: # rings $ref: '#/components/schemas/EncodedPolyline' RentalZoneRestrictions: type: object required: - vehicleTypeIdxs - rideStartAllowed - rideEndAllowed - rideThroughAllowed properties: vehicleTypeIdxs: type: array description: | List of vehicle types (as indices into the provider's vehicle types array) to which these restrictions apply. If empty, the restrictions apply to all vehicle types of the provider. items: type: integer rideStartAllowed: type: boolean description: whether the ride is allowed to start in this zone rideEndAllowed: type: boolean description: whether the ride is allowed to end in this zone rideThroughAllowed: type: boolean description: whether the ride is allowed to pass through this zone stationParking: type: boolean description: whether vehicles can only be parked at stations in this zone RentalVehicleType: type: object required: - id - formFactor - propulsionType - returnConstraint - returnConstraintGuessed properties: id: type: string description: Unique identifier of the vehicle type (unique within the provider) name: type: string description: Public name of the vehicle type (can be empty) formFactor: $ref: '#/components/schemas/RentalFormFactor' propulsionType: $ref: '#/components/schemas/RentalPropulsionType' returnConstraint: $ref: '#/components/schemas/RentalReturnConstraint' returnConstraintGuessed: type: boolean description: Whether the return constraint was guessed instead of being specified by the rental provider RentalProvider: type: object required: - id - name - groupId - bbox - vehicleTypes - formFactors - defaultRestrictions - globalGeofencingRules properties: id: type: string description: Unique identifier of the rental provider name: type: string description: Name of the provider to be displayed to customers groupId: type: string description: Id of the rental provider group this provider belongs to operator: type: string description: Name of the system operator url: type: string description: URL of the vehicle share system purchaseUrl: type: string description: URL where a customer can purchase a membership color: type: string description: | Color associated with this provider, in hexadecimal RGB format (e.g. "#FF0000" for red). Can be empty. bbox: type: array description: | Bounding box of the area covered by this rental provider, [west, south, east, north] as [lon, lat, lon, lat] minItems: 4 maxItems: 4 items: type: number vehicleTypes: type: array items: $ref: '#/components/schemas/RentalVehicleType' formFactors: type: array description: List of form factors offered by this provider items: $ref: '#/components/schemas/RentalFormFactor' defaultRestrictions: $ref: '#/components/schemas/RentalZoneRestrictions' globalGeofencingRules: type: array items: $ref: '#/components/schemas/RentalZoneRestrictions' RentalProviderGroup: type: object required: - id - name - providers - formFactors properties: id: type: string description: Unique identifier of the rental provider group name: type: string description: Name of the provider group to be displayed to customers color: type: string description: | Color associated with this provider group, in hexadecimal RGB format (e.g. "#FF0000" for red). Can be empty. providers: type: array description: List of rental provider IDs that belong to this group items: type: string formFactors: type: array description: List of form factors offered by this provider group items: $ref: '#/components/schemas/RentalFormFactor' RentalStation: type: object required: - id - providerId - providerGroupId - name - lat - lon - isRenting - isReturning - numVehiclesAvailable - formFactors - vehicleTypesAvailable - vehicleDocksAvailable - bbox properties: id: type: string description: Unique identifier of the rental station providerId: type: string description: Unique identifier of the rental provider providerGroupId: type: string description: Unique identifier of the rental provider group name: type: string description: Public name of the station lat: description: latitude type: number lon: description: longitude type: number address: type: string description: Address where the station is located crossStreet: type: string description: Cross street or landmark where the station is located rentalUriAndroid: type: string description: Rental URI for Android (deep link to the specific station) rentalUriIOS: type: string description: Rental URI for iOS (deep link to the specific station) rentalUriWeb: type: string description: Rental URI for web (deep link to the specific station) isRenting: type: boolean description: true if vehicles can be rented from this station, false if it is temporarily out of service isReturning: type: boolean description: true if vehicles can be returned to this station, false if it is temporarily out of service numVehiclesAvailable: type: integer description: Number of vehicles available for rental at this station formFactors: type: array description: List of form factors available for rental and/or return at this station items: $ref: '#/components/schemas/RentalFormFactor' vehicleTypesAvailable: type: object description: List of vehicle types currently available at this station (vehicle type ID -> count) additionalProperties: type: integer vehicleDocksAvailable: type: object description: List of vehicle docks currently available at this station (vehicle type ID -> count) additionalProperties: type: integer stationArea: $ref: '#/components/schemas/MultiPolygon' bbox: type: array description: | Bounding box of the area covered by this station, [west, south, east, north] as [lon, lat, lon, lat] minItems: 4 maxItems: 4 items: type: number RentalVehicle: type: object required: - id - providerId - providerGroupId - typeId - lat - lon - formFactor - propulsionType - returnConstraint - isReserved - isDisabled properties: id: type: string description: Unique identifier of the rental vehicle providerId: type: string description: Unique identifier of the rental provider providerGroupId: type: string description: Unique identifier of the rental provider group typeId: type: string description: Vehicle type ID (unique within the provider) lat: description: latitude type: number lon: description: longitude type: number formFactor: $ref: '#/components/schemas/RentalFormFactor' propulsionType: $ref: '#/components/schemas/RentalPropulsionType' returnConstraint: $ref: '#/components/schemas/RentalReturnConstraint' stationId: type: string description: Station ID if the vehicle is currently at a station homeStationId: type: string description: Station ID where the vehicle must be returned, if applicable isReserved: type: boolean description: true if the vehicle is currently reserved by a customer, false otherwise isDisabled: type: boolean description: true if the vehicle is out of service, false otherwise rentalUriAndroid: type: string description: Rental URI for Android (deep link to the specific vehicle) rentalUriIOS: type: string description: Rental URI for iOS (deep link to the specific vehicle) rentalUriWeb: type: string description: Rental URI for web (deep link to the specific vehicle) RentalZone: type: object required: - providerId - providerGroupId - z - bbox - area - rules properties: providerId: type: string description: Unique identifier of the rental provider providerGroupId: type: string description: Unique identifier of the rental provider group name: type: string description: Public name of the geofencing zone z: type: integer description: Zone precedence / z-index (higher number = higher precedence) bbox: type: array description: | Bounding box of the area covered by this zone, [west, south, east, north] as [lon, lat, lon, lat] minItems: 4 maxItems: 4 items: type: number area: $ref: '#/components/schemas/MultiPolygon' rules: type: array items: $ref: '#/components/schemas/RentalZoneRestrictions' Category: type: object required: - id - name - shortName description: | not available for GTFS datasets by default For NeTEx it contains information about the vehicle category, e.g. IC/InterCity properties: id: type: string name: type: string shortName: type: string Leg: type: object required: - mode - startTime - endTime - scheduledStartTime - scheduledEndTime - realTime - scheduled - duration - from - to - legGeometry properties: mode: $ref: '#/components/schemas/Mode' description: Transport mode for this leg from: $ref: '#/components/schemas/Place' to: $ref: '#/components/schemas/Place' duration: description: | Leg duration in seconds If leg is footpath: The footpath duration is derived from the default footpath duration using the query parameters `transferTimeFactor` and `additionalTransferTime` as follows: `leg.duration = defaultDuration * transferTimeFactor + additionalTransferTime.` In case the defaultDuration is needed, it can be calculated by `defaultDuration = (leg.duration - additionalTransferTime) / transferTimeFactor`. Note that the default values are `transferTimeFactor = 1` and `additionalTransferTime = 0` in case they are not explicitly provided in the query. type: integer startTime: type: string format: date-time description: leg departure time endTime: type: string format: date-time description: leg arrival time scheduledStartTime: type: string format: date-time description: scheduled leg departure time scheduledEndTime: type: string format: date-time description: scheduled leg arrival time realTime: description: Whether there is real-time data about this leg type: boolean scheduled: description: | Whether this leg was originally scheduled to run or is an additional service. Scheduled times will equal realtime times in this case. type: boolean distance: description: For non-transit legs the distance traveled while traversing this leg in meters. type: number interlineWithPreviousLeg: description: For transit legs, if the rider should stay on the vehicle as it changes route names. type: boolean headsign: description: | For transit legs, the headsign of the bus or train being used. For non-transit legs, null type: string tripFrom: description: first stop of this trip $ref: '#/components/schemas/Place' tripTo: description: final stop of this trip (can differ from headsign) $ref: '#/components/schemas/Place' category: $ref: '#/components/schemas/Category' routeId: type: string routeUrl: type: string directionId: type: string routeColor: type: string routeTextColor: type: string routeType: type: integer agencyName: type: string agencyUrl: type: string agencyId: type: string tripId: type: string routeShortName: type: string routeLongName: type: string tripShortName: type: string displayName: type: string cancelled: description: Whether this trip is cancelled type: boolean source: description: Filename and line number where this trip is from type: string intermediateStops: description: | For transit legs, intermediate stops between the Place where the leg originates and the Place where the leg ends. For non-transit legs, null. type: array items: $ref: "#/components/schemas/Place" legGeometry: description: | Encoded geometry of the leg. If detailed leg output is disabled, this is returned as an empty polyline. $ref: '#/components/schemas/EncodedPolyline' steps: description: | A series of turn by turn instructions used for walking, biking and driving. This field is omitted if the request disables detailed leg output. type: array items: $ref: '#/components/schemas/StepInstruction' rental: $ref: '#/components/schemas/Rental' fareTransferIndex: type: integer description: | Index into `Itinerary.fareTransfers` array to identify which fare transfer this leg belongs to effectiveFareLegIndex: type: integer description: | Index into the `Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts` array to identify which effective fare leg this itinerary leg belongs to alerts: description: Alerts for this stop. type: array items: $ref: '#/components/schemas/Alert' loopedCalendarSince: description: | If set, this attribute indicates that this trip has been expanded beyond the feed end date (enabled by config flag `timetable.dataset.extend_calendar`) by looping active weekdays, e.g. from calendar.txt in GTFS. type: string format: date-time bikesAllowed: description: | Whether bikes can be carried on this leg. type: boolean RiderCategory: type: object required: - riderCategoryName - isDefaultFareCategory properties: riderCategoryName: description: Rider category name as displayed to the rider. type: string isDefaultFareCategory: description: Specifies if this category should be considered the default (i.e. the main category displayed to riders). type: boolean eligibilityUrl: description: URL to a web page providing detailed information about the rider category and/or its eligibility criteria. type: string FareMediaType: type: string enum: [ "NONE", "PAPER_TICKET", "TRANSIT_CARD", "CONTACTLESS_EMV", "MOBILE_APP" ] description: | - `NONE`: No fare media involved (e.g., cash payment) - `PAPER_TICKET`: Physical paper ticket - `TRANSIT_CARD`: Physical transit card with stored value - `CONTACTLESS_EMV`: cEMV (contactless payment) - `MOBILE_APP`: Mobile app with virtual transit cards/passes FareMedia: type: object required: - fareMediaType properties: fareMediaName: description: Name of the fare media. Required for transit cards and mobile apps. type: string fareMediaType: description: The type of fare media. $ref: '#/components/schemas/FareMediaType' FareProduct: type: object required: - name - amount - currency properties: name: description: The name of the fare product as displayed to riders. type: string amount: description: The cost of the fare product. May be negative to represent transfer discounts. May be zero to represent a fare product that is free. type: number currency: description: ISO 4217 currency code. The currency of the cost of the fare product. type: string riderCategory: $ref: '#/components/schemas/RiderCategory' media: $ref: '#/components/schemas/FareMedia' FareTransferRule: type: string enum: - A_AB - A_AB_B - AB FareTransfer: type: object description: | The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt Terminology: - **Leg**: An itinerary leg as described by the `Leg` type of this API description. - **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*. - **Fare Transfer**: A fare transfer groups two or more effective fare legs. - **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer - **B** is any *effective fare leg* following the first *effective fare leg* in this transfer - **AB** are all changes between *effective fare legs* contained in this transfer The fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer: - A_AB means that any product from the first effective fare leg combined with the product attached to the transfer itself (AB) which can be empty (= free). Note that all subsequent effective fare leg products need to be ignored in this case. - A_AB_B mean that a product for each effective fare leg needs to be purchased in a addition to the product attached to the transfer itself (AB) which can be empty (= free) - AB only the transfer product itself has to be purchased. Note that all fare products attached to the contained effective fare legs need to be ignored in this case. An itinerary `Leg` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to. required: - effectiveFareLegProducts properties: rule: $ref: '#/components/schemas/FareTransferRule' transferProducts: type: array items: $ref: '#/components/schemas/FareProduct' effectiveFareLegProducts: description: | Lists all valid fare products for the effective fare legs. This is an `array>` where the inner array lists all possible fare products that would cover this effective fare leg. Each "effective fare leg" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc. You can see the outer array as AND (you need one ticket for each effective fare leg (`A_AB_B`), the first effective fare leg (`A_AB`) or no fare leg at all but only the transfer product (`AB`) and the inner array as OR (you can choose which ticket to buy) type: array items: type: array items: type: array items: $ref: '#/components/schemas/FareProduct' Itinerary: type: object required: - duration - startTime - endTime - transfers - legs properties: duration: description: journey duration in seconds type: integer startTime: type: string format: date-time description: journey departure time endTime: type: string format: date-time description: journey arrival time transfers: type: integer description: The number of transfers this trip has. legs: description: Journey legs type: array items: $ref: '#/components/schemas/Leg' fareTransfers: description: Fare information type: array items: $ref: '#/components/schemas/FareTransfer' Transfer: description: transfer from one location to another type: object required: - to properties: to: $ref: '#/components/schemas/Place' default: type: number description: | optional; missing if the GTFS did not contain a transfer transfer duration in minutes according to GTFS (+heuristics) foot: type: number description: | optional; missing if no path was found (timetable / osr) transfer duration in minutes for the foot profile footRouted: type: number description: | optional; missing if no path was found with foot routing transfer duration in minutes for the foot profile wheelchair: type: number description: | optional; missing if no path was found with the wheelchair profile transfer duration in minutes for the wheelchair profile wheelchairRouted: type: number description: | optional; missing if no path was found with the wheelchair profile transfer duration in minutes for the wheelchair profile wheelchairUsesElevator: type: boolean description: | optional; missing if no path was found with the wheelchair profile true if the wheelchair path uses an elevator car: type: number description: | optional; missing if no path was found with car routing transfer duration in minutes for the car profile OneToManyParams: type: object required: - one - many - mode - max - maxMatchingDistance - arriveBy properties: one: description: geo location as latitude;longitude type: string many: description: | geo locations as latitude;longitude,latitude;longitude,... The number of accepted locations is limited by server config variable `onetomany_max_many`. type: array items: type: string explode: false mode: description: | routing profile to use (currently supported: \`WALK\`, \`BIKE\`, \`CAR\`) $ref: '#/components/schemas/Mode' max: description: maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`. type: number maxMatchingDistance: description: maximum matching distance in meters to match geo coordinates to the street network type: number elevationCosts: description: | Optional. Default is `NONE`. Set an elevation cost profile, to penalize routes with incline. - `NONE`: No additional costs for elevations. This is the default behavior - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. Elevation cost profiles are currently used by following street modes: - `BIKE` $ref: '#/components/schemas/ElevationCosts' default: NONE arriveBy: description: | true = many to one false = one to many type: boolean withDistance: description: | If true, the response includes the distance in meters for each path. This requires path reconstruction and may be slower than duration-only queries. type: boolean default: false OneToManyIntermodalParams: type: object required: - one - many properties: one: description: | \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id type: string many: description: | array of: \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id The number of accepted locations is limited by server config variable `onetomany_max_many`. type: array items: type: string explode: false time: description: | Optional. Defaults to the current time. Departure time ($arriveBy=false) / arrival date ($arriveBy=true), type: string format: date-time maxTravelTime: description: | The maximum travel time in minutes. If not provided, the routing uses the value hardcoded in the server which is usually quite high. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the least transfers) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. type: integer maxMatchingDistance: description: maximum matching distance in meters to match geo coordinates to the street network type: number default: 25 arriveBy: description: | Optional. Defaults to false, i.e. one to many search true = many to one false = one to many type: boolean default: false maxTransfers: description: | The maximum number of allowed transfers (i.e. interchanges between transit legs, pre- and postTransit do not count as transfers). `maxTransfers=0` searches for direct transit connections without any transfers. If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), send an empty `transitModes` parameter instead. If not provided, the routing uses the server-side default value which is hardcoded and very high to cover all use cases. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the fastest) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. type: integer minTransferTime: description: | Optional. Default is 0 minutes. Minimum transfer time for each transfer in minutes. type: integer default: 0 additionalTransferTime: description: | Optional. Default is 0 minutes. Additional transfer time reserved for each transfer in minutes. type: integer default: 0 transferTimeFactor: description: | Optional. Default is 1.0 Factor to multiply minimum required transfer times with. Values smaller than 1.0 are not supported. type: number default: 1.0 useRoutedTransfers: description: | Optional. Default is `false`. Whether to use transfers routed on OpenStreetMap data. type: boolean default: false pedestrianProfile: description: | Optional. Default is `FOOT`. Accessibility profile to use for pedestrian routing in transfers between transit connections and the first and last mile respectively. $ref: "#/components/schemas/PedestrianProfile" default: FOOT pedestrianSpeed: description: | Optional Average speed for pedestrian routing. $ref: "#/components/schemas/PedestrianSpeed" cyclingSpeed: description: | Optional Average speed for bike routing. $ref: "#/components/schemas/CyclingSpeed" elevationCosts: description: | Optional. Default is `NONE`. Set an elevation cost profile, to penalize routes with incline. - `NONE`: No additional costs for elevations. This is the default behavior - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. The profile is used for routing on both the first and last mile. Elevation cost profiles are currently used by following street modes: - `BIKE` $ref: "#/components/schemas/ElevationCosts" default: NONE transitModes: description: | Optional. Default is `TRANSIT` which allows all transit modes (no restriction). Allowed modes for the transit part. If empty, no transit connections will be computed. For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. type: array items: $ref: "#/components/schemas/Mode" default: - TRANSIT explode: false preTransitModes: description: | Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). A list of modes that are allowed to be used for the first mile, i.e. from the coordinates to the first transit stop. Example: `WALK,BIKE_SHARING`. type: array items: $ref: "#/components/schemas/Mode" default: - WALK explode: false postTransitModes: description: | Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). A list of modes that are allowed to be used for the last mile, i.e. from the last transit stop to the target coordinates. Example: `WALK,BIKE_SHARING`. type: array items: $ref: "#/components/schemas/Mode" default: - WALK explode: false directMode: description: | Default is `WALK` which will compute walking routes as direct connections. Mode used for direction connections from start to destination without using transit. Currently supported non-transit modes: \`WALK\`, \`BIKE\`, \`CAR\` $ref: "#/components/schemas/Mode" default: WALK maxPreTransitTime: description: | Optional. Default is 15min which is `900`. Maximum time in seconds for the first street leg. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. type: integer default: 900 minimum: 0 maxPostTransitTime: description: | Optional. Default is 15min which is `900`. Maximum time in seconds for the last street leg. Is limited by server config variable `street_routing_max_prepost_transit_seconds`. type: integer default: 900 minimum: 0 maxDirectTime: description: | Optional. Default is 30min which is `1800`. Maximum time in seconds for direct connections. If a value smaller than either `maxPreTransitTime` or `maxPostTransitTime` is used, their maximum is set instead. Is limited by server config variable `street_routing_max_direct_seconds`. type: integer default: 1800 minimum: 0 withDistance: description: | If true, the response includes the distance in meters for each path. This requires path reconstruction and may be slower than duration-only queries. `withDistance` is currently limited to street routing. type: boolean default: false requireBikeTransport: description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow bike carriage. type: boolean default: false requireCarTransport: description: | Optional. Default is `false`. If set to `true`, all used transit trips are required to allow car carriage. type: boolean default: false ServerConfig: Description: server configuration type: object required: - motisVersion - hasElevation - hasRoutedTransfers - hasStreetRouting - maxOneToManySize - maxOneToAllTravelTimeLimit - maxPrePostTransitTimeLimit - maxDirectTimeLimit - shapesDebugEnabled properties: motisVersion: description: the version of this MOTIS server type: string hasElevation: description: true if elevation is loaded type: boolean hasRoutedTransfers: description: true if routed transfers available type: boolean hasStreetRouting: description: true if street routing is available type: boolean maxOneToManySize: description: | limit for the number of `many` locations for one-to-many requests type: number maxOneToAllTravelTimeLimit: description: limit for maxTravelTime API param in minutes type: number maxPrePostTransitTimeLimit: description: limit for maxPrePostTransitTime API param in seconds type: number maxDirectTimeLimit: description: limit for maxDirectTime API param in seconds type: number shapesDebugEnabled: description: true if experimental route shapes debug download API is enabled type: boolean Error: type: object required: - error properties: error: type: string description: error message RouteSegment: description: Route segment between two stops to show a route on a map type: object required: - from - to - polyline properties: from: type: integer description: Index into the top-level route stops array to: type: integer description: Index into the top-level route stops array polyline: type: integer description: Index into the top-level route polylines array RoutePolyline: description: Shared polyline used by one or more route segments type: object required: - polyline - colors - routeIndexes properties: polyline: $ref: '#/components/schemas/EncodedPolyline' colors: type: array description: Unique route colors of routes containing this segment items: type: string routeIndexes: type: array description: Indexes into the top-level routes array for routes containing this segment items: type: integer RouteColor: type: object required: - color - textColor properties: color: type: string textColor: type: string RoutePathSource: type: string enum: - NONE - TIMETABLE - ROUTED TransitRouteInfo: type: object required: - id - shortName - longName properties: id: type: string shortName: type: string longName: type: string color: type: string textColor: type: string RouteInfo: type: object required: - mode - transitRoutes - numStops - routeIdx - pathSource - segments properties: mode: $ref: '#/components/schemas/Mode' description: Transport mode for this route transitRoutes: type: array items: $ref: '#/components/schemas/TransitRouteInfo' numStops: type: integer description: Number of stops along this route routeIdx: type: integer description: Internal route index for debugging purposes pathSource: $ref: '#/components/schemas/RoutePathSource' segments: type: array items: $ref: '#/components/schemas/RouteSegment' ================================================ FILE: src/adr_extend_tt.cc ================================================ #include "osr/geojson.h" #include "motis/adr_extend_tt.h" #include "nigiri/special_stations.h" #include #include #include #include "utl/get_or_create.h" #include "utl/parallel_for.h" #include "utl/timer.h" #include "utl/to_vec.h" #include "nigiri/timetable.h" #include "nigiri/translations_view.h" #include "adr/area_database.h" #include "adr/score.h" #include "adr/typeahead.h" #include "motis/types.h" namespace a = adr; namespace n = nigiri; namespace json = boost::json; namespace motis { constexpr auto const kClaszMax = static_cast>(n::kNumClasses); date::time_zone const* get_tz(n::timetable const& tt, adr_ext const* ae, tz_map_t const* tz, n::location_idx_t const l) { auto const p = tt.locations_.parents_[l]; auto const x = p == n::location_idx_t::invalid() ? l : p; auto const p_idx = !ae || !tz ? adr_extra_place_idx_t::invalid() : ae->location_place_.at(x); if (p_idx != adr_extra_place_idx_t::invalid()) { return tz->at(p_idx); } return nullptr; } void normalize(std::string& x) { auto const replace_str = [&](std::string_view search, std::string_view replace) { auto const pos = x.find(search); if (pos != std::string::npos) { x.replace(pos, search.size(), replace); } }; replace_str("Int.", "Hbf"); x = adr::normalize(x); auto const removals = std::initializer_list{ "tief", "oben", "gleis", "platform", "gl", "gare", "bahnhof", "bhf", "strasse", "gasse"}; for (auto const r : removals) { auto const pos = x.find(r); if (pos != std::string::npos) { x.erase(pos, r.size()); } } auto const replacements = std::initializer_list>{ {"flixtrain", "hbf"}, {"hauptbf", "hbf"}, {"haupt", "hbf"}, {"centrale", "hbf"}, {"station", "hbf"}, {"zob", "hbf"}, {"int", "hbf"}, {"international", "hbf"}, {"anleger", "fähre"}}; for (auto const& [search, replace] : replacements) { replace_str(search, replace); } } adr::score_t get_diff(std::string str1, std::string str2, std::vector& sift4_offset_arr) { str1 = adr::normalize(str1); str2 = adr::normalize(str2); normalize(str1); normalize(str2); if (str1.empty() && str2.empty()) { return 0; } if (str1.contains("hbf") && str2.contains("hbf")) { return 0; } auto a = std::vector{}; auto b = std::vector{}; adr::for_each_token( str1, [&](auto&& p_token) mutable { if (!p_token.empty()) { a.emplace_back(p_token); } return utl::continue_t::kContinue; }, ' ', '-'); adr::for_each_token( str2, [&](auto&& p_token) mutable { if (!p_token.empty()) { b.emplace_back(p_token); } return utl::continue_t::kContinue; }, ' ', '-'); auto covered = std::uint32_t{}; auto score = adr::score_t{0U}; for (auto i = 0U; i != a.size(); ++i) { auto best = std::numeric_limits::max(); auto best_j = 0U; for (auto j = 0U; j != b.size(); ++j) { if ((covered & (1U << j)) != 0U) { continue; } auto const dist = std::min( static_cast(std::max(a[i].size(), b[i].size())), adr::sift4(a[i], b[j], 3, static_cast( std::min(a[i].size(), b[j].size()) / 2U + 2U), sift4_offset_arr)); if (dist < best) { best = dist; best_j = j; } } covered |= (1U << best_j); score += best; } for (auto j = 0U; j != b.size(); ++j) { if ((covered & (1U << j)) == 0U) { score += b[j].size(); } } return static_cast(score) / static_cast(std::max(str1.length(), str2.length())); } adr_ext adr_extend_tt(nigiri::timetable const& tt, a::area_database const* area_db, a::typeahead& t) { if (tt.n_locations() == 0) { return {}; } auto const timer = utl::scoped_timer{"guesser candidates"}; auto ret = adr_ext{}; auto area_set_lookup = [&]() { auto x = hash_map, a::area_set_idx_t>{}; for (auto const [i, area_set] : utl::enumerate(t.area_sets_)) { x.emplace(area_set.view(), a::area_set_idx_t{i}); } return x; }(); // mapping: location_idx -> place_idx // reverse: place_idx -> location_idx auto place_location = n::vecvec{}; auto const add_place = [&](n::location_idx_t const l) { auto const i = adr_extra_place_idx_t{place_location.size()}; ret.location_place_[l] = i; place_location.emplace_back({l}); return i; }; auto const get_transitive_equivalences = [&](n::location_idx_t const l) { auto q = std::vector{}; auto visited = hash_set{}; auto const visit = [&](n::location_idx_t const x) { auto const [_, inserted] = visited.insert(x); if (!inserted) { return; } for (auto const eq : tt.locations_.equivalences_[x]) { if (!visited.contains(eq)) { q.push_back(eq); } } }; visit(l); while (!q.empty()) { auto const next = q.back(); q.resize(q.size() - 1); visit(next); } return visited; }; { ret.location_place_.resize(tt.n_locations(), adr_extra_place_idx_t::invalid()); // Map each location + its equivalents with the same name to one // place_idx. auto sift4_dist = std::vector{}; for (auto l = n::location_idx_t{nigiri::kNSpecialStations}; l != tt.n_locations(); ++l) { if (ret.location_place_[l] != adr_extra_place_idx_t::invalid() || tt.locations_.parents_[l] != n::location_idx_t::invalid()) { continue; } auto const place_idx = add_place(l); auto const name = tt.get_default_translation(tt.locations_.names_[l]); for (auto const eq : get_transitive_equivalences(l)) { if (ret.location_place_[eq] != adr_extra_place_idx_t::invalid() || tt.locations_.parents_[eq] != n::location_idx_t::invalid()) { continue; } if (tt.get_default_translation(tt.locations_.names_[eq]) == name) { ret.location_place_[eq] = place_idx; } else { auto const dist = geo::distance(tt.locations_.coordinates_[l], tt.locations_.coordinates_[eq]); auto const eq_name = tt.get_default_translation(tt.locations_.names_[eq]); auto const str_diff = get_diff(std::string{name}, std::string{eq_name}, sift4_dist); auto const cutoff = (500.F - 1750.F * str_diff); auto const good = dist < cutoff; if (good) { ret.location_place_[eq] = place_idx; auto existing = place_location.back(); if (utl::find_if(existing, [&](n::location_idx_t const x) { return tt.get_default_translation(tt.locations_.names_[x]) == tt.get_default_translation(tt.locations_.names_[eq]); }) == end(existing)) { place_location.back().push_back(eq); } } } } } // Map all children to root. for (auto l = n::location_idx_t{0U}; l != tt.n_locations(); ++l) { if (tt.locations_.parents_[l] != n::location_idx_t::invalid()) { ret.location_place_[l] = ret.location_place_[tt.locations_.get_root_idx(l)]; } } } for (auto const [i, place] : utl::enumerate(ret.location_place_)) { if (place == adr_extra_place_idx_t::invalid()) { place = adr_extra_place_idx_t{0U}; } } for (auto const [i, p] : utl::enumerate(ret.location_place_)) { auto const l = n::location_idx_t{i}; if (l >= n::kNSpecialStations && p == adr_extra_place_idx_t::invalid()) { auto const parent = tt.locations_.parents_[l] == n::location_idx_t::invalid() ? n::get_special_station(n::special_station::kEnd) : tt.locations_.parents_[l]; auto const grand_parent = tt.locations_.parents_[parent] == n::location_idx_t::invalid() ? n::get_special_station(n::special_station::kEnd) : tt.locations_.parents_[parent]; utl::log_error( "adr_extend", "invalid place for {} (parent={}, grand_parent={})", n::loc{tt, l}, n::loc{tt, parent}, n::loc{tt, grand_parent}); ret.location_place_[l] = adr_extra_place_idx_t{0U}; } } // For each station without parent: // Compute importance = transport count weighted by clasz. ret.place_importance_.resize(place_location.size()); ret.place_clasz_.resize(place_location.size()); { auto const event_counts = utl::scoped_timer{"guesser event_counts"}; for (auto i = n::kNSpecialStations; i < tt.n_locations(); ++i) { auto const l = n::location_idx_t{i}; auto transport_counts = std::array{}; for (auto const& r : tt.location_routes_[l]) { auto const clasz = static_cast>(tt.route_clasz_[r]); for (auto const tr : tt.route_transport_ranges_[r]) { transport_counts[clasz] += tt.bitfields_[tt.transport_traffic_days_[tr]].count(); } } constexpr auto const prio = std::array{/* Air */ 300, /* HighSpeed */ 300, /* LongDistance */ 250, /* Coach */ 150, /* Night */ 250, /* RideSharing */ 5, /* Regional */ 100, /* Suburban */ 80, /* Subway */ 80, /* Tram */ 3, /* Bus */ 2, /* Ship */ 10, /* CableCar */ 5, /* Funicular */ 5, /* AerialLift */ 5, /* Other */ 1}; auto const root = tt.locations_.get_root_idx(l); auto const place_idx = ret.location_place_[root]; for (auto const [clasz, t_count] : utl::enumerate(transport_counts)) { ret.place_importance_[place_idx] += prio[clasz] * static_cast(t_count); auto const c = n::clasz{static_cast(clasz)}; if (t_count != 0U) { ret.place_clasz_[place_idx] |= n::routing::to_mask(c); } } } } // Update counts of meta-stations with the sum of their priorities. // Meta stations have equivalence relations to other stops and are at (0,0) for (auto i = n::kNSpecialStations; i < tt.n_locations(); ++i) { auto const l = n::location_idx_t{i}; auto const is_meta = tt.locations_.coordinates_[l] == geo::latlng{} && tt.locations_.parents_[l] == n::location_idx_t::invalid() && !tt.locations_.equivalences_[l].empty(); if (!is_meta) { continue; } auto const place_idx = ret.location_place_[l]; for (auto const eq : get_transitive_equivalences(l)) { auto const eq_root = tt.locations_.get_root_idx(eq); auto const eq_place_idx = ret.location_place_[eq_root]; ret.place_importance_[place_idx] += ret.place_importance_[eq_place_idx]; ret.place_clasz_[place_idx] |= ret.place_clasz_[eq_place_idx]; } } utl::verify(!ret.place_importance_.empty(), "no places"); // Normalize to interval [0, 1] by dividing by max. importance. { auto const normalize = utl::scoped_timer{"guesser normalize"}; auto const max_it = std::max_element(begin(ret.place_importance_), end(ret.place_importance_)); auto const max_importance = std::max(*max_it, 1.F); for (auto& i : ret.place_importance_) { i /= max_importance; } } // Add to typeahead. auto const add_string = [&](std::string_view s, a::place_idx_t const place_idx) { auto const str_idx = a::string_idx_t{t.strings_.size()}; t.strings_.emplace_back(s); t.string_to_location_.emplace_back( std::initializer_list{to_idx(place_idx)}); t.string_to_type_.emplace_back( std::initializer_list{a::location_type_t::kPlace}); return str_idx; }; auto areas = basic_string{}; auto no_areas_idx = adr::area_set_idx_t{t.area_sets_.size()}; if (area_db == nullptr) { t.area_sets_.emplace_back(areas); } for (auto const [prio, locations] : utl::zip(ret.place_importance_, place_location)) { auto const place_idx = a::place_idx_t{t.place_names_.size()}; auto names = std::vector>{}; auto const add_names = [&](n::location_idx_t const loc) { for (auto const [lang, text] : n::get_translation_view(tt, tt.locations_.names_[loc])) { names.emplace_back(add_string(text, place_idx), t.get_or_create_lang_idx(tt.languages_.get(lang))); } }; for (auto const l : locations) { add_names(l); for (auto const& c : tt.locations_.children_[l]) { if (tt.locations_.types_[c] == nigiri::location_type::kStation && tt.get_default_translation(tt.locations_.names_[c]) != tt.get_default_translation(tt.locations_.names_[l])) { add_names(c); } } auto const is_null_island = [](geo::latlng const& pos) { return pos.lat() < 3.0 && pos.lng() < 3.0; }; auto pos = tt.locations_.coordinates_[l]; if (is_null_island(pos)) { for (auto const c : tt.locations_.children_[l]) { if (!is_null_island(tt.locations_.coordinates_[c])) { pos = tt.locations_.coordinates_[c]; break; } } } } auto const pos = a::coordinates::from_latlng(tt.locations_.coordinates_[locations[0]]); fmt::println(std::clog, "names: {}, stops={}, prio={}", names | std::views::transform([&](auto&& n) { return t.strings_[n.first].view(); }), locations | std::views::transform( [&](auto&& l) { return n::loc{tt, l}; }), prio); t.place_type_.emplace_back(a::amenity_category::kExtra); t.place_names_.emplace_back( names | std::views::transform([](auto&& n) { return n.first; })); t.place_name_lang_.emplace_back( names | std::views::transform([](auto&& n) { return n.second; })); t.place_coordinates_.emplace_back(pos); t.place_osm_ids_.emplace_back( locations | std::views::transform([&](auto&& l) { return to_idx(l); })); t.place_population_.emplace_back(static_cast( (prio * 1'000'000) / a::population::kCompressionFactor)); t.place_is_way_.resize(t.place_is_way_.size() + 1U); if (area_db == nullptr) { t.place_areas_.emplace_back(no_areas_idx); } else { area_db->lookup(t, a::coordinates::from_latlng(pos), areas); t.place_areas_.emplace_back( utl::get_or_create(area_set_lookup, areas, [&]() { auto const set_idx = a::area_set_idx_t{t.area_sets_.size()}; t.area_sets_.emplace_back(areas); return set_idx; })); } } t.build_ngram_index(); return ret; } } // namespace motis ================================================ FILE: src/analyze_shapes.cc ================================================ #include "motis/analyze_shapes.h" #include "utl/verify.h" #include "fmt/base.h" #include "fmt/ranges.h" #include "nigiri/shapes_storage.h" #include "nigiri/types.h" #include "motis/tag_lookup.h" namespace motis { bool analyze_shape(nigiri::shapes_storage const& shapes, std::string const& trip_id, nigiri::trip_idx_t const& trip_idx) { auto const offset_idx = shapes.trip_offset_indices_[trip_idx].second; if (offset_idx == nigiri::shape_offset_idx_t::invalid()) { fmt::println("No shape offsets for trip-id '{}'\n", trip_id); return false; } auto const offsets = shapes.offsets_[offset_idx]; if (offsets.empty()) { fmt::println("Empty shape for trip-id '{}'\n", trip_id); return false; } fmt::println("Offsets for trip-id '{}':\n{}\n", trip_id, offsets); return true; } bool analyze_shapes(data const& d, std::vector const& trip_ids) { utl::verify(d.tt_ != nullptr, "Missing timetable"); utl::verify(d.tags_ != nullptr, "Missing tags"); utl::verify(d.shapes_ != nullptr, "Missing shapes"); auto success = true; for (auto const& trip_id : trip_ids) { auto const [run, trip_idx] = d.tags_->get_trip(*d.tt_, nullptr, trip_id); if (!run.valid()) { success = false; fmt::println("Failed to find trip-id '{}'\n", trip_id); continue; } success &= analyze_shape(*d.shapes_, trip_id, trip_idx); } return success; } } // namespace motis ================================================ FILE: src/clog_redirect.cc ================================================ #include "motis/clog_redirect.h" #include #include namespace motis { namespace { struct synchronized_streambuf : std::streambuf { synchronized_streambuf(std::streambuf* wrapped, std::mutex& mutex) : wrapped_{wrapped}, mutex_{mutex} {} int_type overflow(int_type ch) override { auto const lock = std::lock_guard{mutex_}; if (traits_type::eq_int_type(ch, traits_type::eof())) { return wrapped_->pubsync() == 0 ? traits_type::not_eof(ch) : traits_type::eof(); } return wrapped_->sputc(traits_type::to_char_type(ch)); } std::streamsize xsputn(char const* s, std::streamsize count) override { auto const lock = std::lock_guard{mutex_}; return wrapped_->sputn(s, count); } int sync() override { auto const lock = std::lock_guard{mutex_}; return wrapped_->pubsync(); } private: std::streambuf* wrapped_; std::mutex& mutex_; }; } // namespace clog_redirect::clog_redirect(char const* log_file_path) { if (!enabled_) { return; } sink_.exceptions(std::ios_base::badbit | std::ios_base::failbit); sink_.open(log_file_path, std::ios_base::app); sink_buf_ = std::make_unique(sink_.rdbuf(), mutex_); backup_clog_ = std::clog.rdbuf(); std::clog.rdbuf(sink_buf_.get()); active_ = true; } clog_redirect::~clog_redirect() { if (!active_) { return; } auto const lock = std::lock_guard{mutex_}; std::clog.rdbuf(backup_clog_); } void clog_redirect::set_enabled(bool const enabled) { enabled_ = enabled; } // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) bool clog_redirect::enabled_ = true; } // namespace motis ================================================ FILE: src/compute_footpaths.cc ================================================ #include "motis/compute_footpaths.h" #include "nigiri/loader/build_lb_graph.h" #include "cista/mmap.h" #include "cista/serialization.h" #include "utl/concat.h" #include "utl/erase_if.h" #include "utl/parallel_for.h" #include "utl/sorted_diff.h" #include "osr/routing/profiles/foot.h" #include "osr/routing/route.h" #include "osr/util/infinite.h" #include "osr/util/reverse.h" #include "motis/constants.h" #include "motis/get_loc.h" #include "motis/match_platforms.h" #include "motis/osr/max_distance.h" #include "motis/osr/parameters.h" #include "motis/point_rtree.h" namespace n = nigiri; namespace motis { elevator_footpath_map_t compute_footpaths( osr::ways const& w, osr::lookup const& lookup, osr::platforms const& pl, nigiri::timetable& tt, osr::elevation_storage const* elevations, bool const update_coordinates, std::vector const& settings) { fmt::println(std::clog, "creating matches"); auto const matches = get_matches(tt, pl, w); fmt::println(std::clog, " -> creating r-tree"); auto const loc_rtree = [&]() { auto t = point_rtree{}; for (auto i = n::location_idx_t{0U}; i != tt.n_locations(); ++i) { if (update_coordinates && matches[i] != osr::platform_idx_t::invalid()) { auto const center = get_platform_center(pl, w, matches[i]); if (center.has_value() && geo::distance(*center, tt.locations_.coordinates_[i]) < kMaxAdjust) { tt.locations_.coordinates_[i] = *center; } } t.add(tt.locations_.coordinates_[i], i); } return t; }(); auto const pt = utl::get_active_progress_tracker(); pt->in_high(2U * tt.n_locations() * settings.size()); auto elevator_in_paths_mutex = std::mutex{}; auto elevator_in_paths = elevator_footpath_map_t{}; auto const add_if_elevator = [&](osr::node_idx_t const n, n::location_idx_t const a, n::location_idx_t const b) { if (n != osr::node_idx_t::invalid() && w.r_->node_properties_[n].is_elevator()) { auto l = std::unique_lock{elevator_in_paths_mutex}; elevator_in_paths[n].emplace(a, b); } }; struct state { std::vector sorted_tt_fps_; std::vector missing_; std::vector neighbors_; std::vector neighbors_loc_; std::vector neighbor_candidates_; }; auto n_done = 0U; auto candidates = vector_map{}; auto transfers = n::vector_map>( tt.n_locations()); for (auto const& mode : settings) { candidates.clear(); candidates.resize(tt.n_locations()); for (auto& fps : transfers) { fps.clear(); } auto const is_candidate = [&](n::location_idx_t const l) { return !mode.is_candidate_ || mode.is_candidate_(l); }; { auto const timer = utl::scoped_timer{ fmt::format("matching timetable locations for profile={}", to_str(mode.profile_))}; utl::parallel_for_run(tt.n_locations(), [&](std::size_t const x) { pt->update_monotonic(n_done + x); auto const l = n::location_idx_t{static_cast(x)}; if (!is_candidate(l)) { return; } candidates[l] = lookup.match( to_profile_parameters(mode.profile_, {}), get_loc(tt, w, pl, matches, l), false, osr::direction::kForward, mode.max_matching_distance_, nullptr, mode.profile_); }); n_done += tt.n_locations(); } utl::parallel_for_run_threadlocal( tt.n_locations(), [&](state& s, auto const i) { cista::for_each_field(s, [](auto& f) { f.clear(); }); auto const l = n::location_idx_t{i}; if (!is_candidate(l)) { pt->update_monotonic(n_done + i); return; } loc_rtree.in_radius( tt.locations_.coordinates_[l], get_max_distance(mode.profile_, mode.max_duration_), [&](n::location_idx_t const x) { if (x != l && is_candidate(x)) { s.neighbors_.emplace_back(x); } }); auto const results = osr::route( to_profile_parameters(mode.profile_, {}), w, lookup, mode.profile_, get_loc(tt, w, pl, matches, l), utl::transform_to(s.neighbors_, s.neighbors_loc_, [&](n::location_idx_t const x) { return get_loc(tt, w, pl, matches, x); }), candidates[l], utl::transform_to( s.neighbors_, s.neighbor_candidates_, [&](n::location_idx_t const x) { return candidates[x]; }), static_cast(mode.max_duration_.count()), osr::direction::kForward, nullptr, nullptr, elevations, [](osr::path const& p) { return p.uses_elevator_; }); for (auto const [n, r] : utl::zip(s.neighbors_, results)) { if (!r.has_value()) { continue; } auto const duration = n::duration_t{ static_cast(std::ceil(r->cost_ / 60.0))}; transfers[l].emplace_back(n::footpath{n, duration}); if (mode.profile_ == osr::search_profile::kWheelchair) { for (auto const& seg : r->segments_) { add_if_elevator(seg.from_, l, n); add_if_elevator(seg.from_, n, l); } } } if (mode.extend_missing_) { auto const& tt_fps = tt.locations_.footpaths_out_[0].at(l); s.sorted_tt_fps_.resize(tt_fps.size()); std::copy(begin(tt_fps), end(tt_fps), begin(s.sorted_tt_fps_)); utl::sort(s.sorted_tt_fps_); utl::sort(transfers[l]); utl::sorted_diff( s.sorted_tt_fps_, transfers[l], [](auto&& a, auto&& b) { return a.target() < b.target(); }, [](auto&& a, auto&& b) { return a.target() == b.target(); }, utl::overloaded{ [](n::footpath, n::footpath) { assert(false); }, [&](utl::op const op, n::footpath const x) { if (op == utl::op::kDel) { auto const dist = geo::distance( tt.locations_.coordinates_[l], tt.locations_.coordinates_[x.target()]); if (dist < 100.0) { auto const duration = n::duration_t{ static_cast(std::ceil((dist / 0.7) / 60.0))}; s.missing_.emplace_back(x.target(), duration); } } }}); utl::concat(transfers[l], s.missing_); } utl::erase_if(transfers[l], [&](n::footpath fp) { return fp.duration() > mode.max_duration_; }); utl::sort(transfers[l]); pt->update_monotonic(n_done + i); }); auto transfers_in = n::vector_map>{}; transfers_in.resize(tt.n_locations()); for (auto const [i, out] : utl::enumerate(transfers)) { auto const l = n::location_idx_t{i}; for (auto const fp : out) { assert(fp.target() < tt.n_locations()); transfers_in[fp.target()].push_back(n::footpath{l, fp.duration()}); } } for (auto const& x : transfers) { tt.locations_.footpaths_out_[mode.profile_idx_].emplace_back(x); } for (auto const& x : transfers_in) { tt.locations_.footpaths_in_[mode.profile_idx_].emplace_back(x); } n::loader::build_lb_graph(tt, mode.profile_idx_); n::loader::build_lb_graph(tt, mode.profile_idx_); n_done += tt.n_locations(); } return elevator_in_paths; } } // namespace motis ================================================ FILE: src/config.cc ================================================ #include "motis/config.h" #include #include "boost/url.hpp" #include "fmt/std.h" #include "utl/erase.h" #include "utl/overloaded.h" #include "utl/read_file.h" #include "utl/verify.h" #include "nigiri/clasz.h" #include "nigiri/routing/limits.h" #include "rfl.hpp" #include "rfl/yaml.hpp" namespace fs = std::filesystem; namespace motis { template consteval auto drop_last() { return [](std::index_sequence) { return rfl::internal::StringLiteral(Name.arr_[Is]...); }(std::make_index_sequence{}); } struct drop_trailing { public: template static auto process(auto&& named_tuple) { auto const handle_one = [](FieldType&& f) { if constexpr (FieldType::name() != "xml_content" && !rfl::internal::is_rename_v) { return handle_one_field(std::move(f)); } else { return std::move(f); } }; return named_tuple.transform(handle_one); } private: template static auto handle_one_field(FieldType&& _f) { using NewFieldType = rfl::Field(), typename FieldType::Type>; return NewFieldType(_f.value()); } }; std::ostream& operator<<(std::ostream& out, config const& c) { return out << rfl::yaml::write(c); } config config::read_simple(std::vector const& args) { auto c = config{}; for (auto const& arg : args) { auto const p = fs::path{arg}; utl::verify(fs::exists(p), "path {} does not exist", p); if (fs::is_regular_file(p) && p.generic_string().ends_with("osm.pbf")) { c.osm_ = p; c.street_routing_ = true; c.geocoding_ = true; c.reverse_geocoding_ = true; c.tiles_ = {config::tiles{.profile_ = "tiles-profiles/full.lua"}}; } else { if (!c.timetable_.has_value()) { c.timetable_ = {timetable{.railviz_ = true}}; } auto tag = p.stem().generic_string(); utl::erase(tag, '_'); utl::erase(tag, '.'); c.timetable_->datasets_.emplace( tag, timetable::dataset{.path_ = p.generic_string()}); } } return c; } config config::read(std::filesystem::path const& p) { auto const file_content = utl::read_file(p.generic_string().c_str()); utl::verify(file_content.has_value(), "could not read config file at {}", p); return read(*file_content); } config config::read(std::string const& s) { auto c = rfl::yaml::read(s).value(); if (!c.limits_.has_value()) { c.limits_.emplace(limits{}); } c.verify(); return c; } void config::verify() const { auto const street_routing = use_street_routing(); utl::verify(!tiles_ || osm_, "feature TILES requires OpenStreetMap data"); utl::verify(!street_routing || osm_, "feature STREET_ROUTING requires OpenStreetMap data"); utl::verify(!timetable_ || !timetable_->datasets_.empty(), "feature TIMETABLE requires timetable data"); utl::verify( !osr_footpath_ || (street_routing && timetable_), "feature OSR_FOOTPATH requires features STREET_ROUTING and TIMETABLE"); utl::verify(!has_elevators() || (street_routing && timetable_), "feature ELEVATORS requires STREET_ROUTING and TIMETABLE"); utl::verify(!has_gbfs_feeds() || street_routing, "feature GBFS requires feature STREET_ROUTING"); utl::verify(!has_prima() || (street_routing && timetable_), "feature ODM requires feature STREET_ROUTING"); utl::verify(!has_elevators() || osr_footpath_, "feature ELEVATORS requires feature OSR_FOOTPATHS"); utl::verify(limits_.value().plan_max_search_window_minutes_ <= nigiri::routing::kMaxSearchIntervalSize.count(), "plan_max_search_window_minutes limit cannot be above {}", nigiri::routing::kMaxSearchIntervalSize.count()); utl::verify(limits_.value().geocode_max_suggestions_ >= 1U, "geocode_max_suggestions must be >= 1"); utl::verify(limits_.value().reverse_geocode_max_results_ >= 1U, "reverse_geocode_max_results must be >= 1"); if (timetable_) { utl::verify(!timetable_->route_shapes_.has_value() || (timetable_->with_shapes_ && street_routing), "feature ROUTE_SHAPES requires SHAPES and STREET_ROUTING"); for (auto const& [id, d] : timetable_->datasets_) { utl::verify(!id.contains("_"), "dataset identifier may not contain '_'"); if (d.rt_.has_value()) { for (auto const& rt : *d.rt_) { try { boost::urls::url{rt.url_}; } catch (std::exception const& e) { throw utl::fail("{} is not a valid url: {}", rt.url_, e.what()); } utl::verify(rt.protocol_ != timetable::dataset::rt::protocol::auser || timetable_->incremental_rt_update_, "VDV AUS requires incremental RT update scheme"); } } } } } void config::verify_input_files_exist() const { utl::verify(!osm_ || fs::is_regular_file(*osm_), "OpenStreetMap file does not exist: {}", osm_.value_or(fs::path{})); utl::verify(!tiles_ || fs::is_regular_file(tiles_->profile_), "tiles profile {} does not exist", tiles_.value_or(tiles{}).profile_); utl::verify(!tiles_ || !tiles_->coastline_ || fs::is_regular_file(*tiles_->coastline_), "coastline file {} does not exist", tiles_.value_or(tiles{}).coastline_.value_or("")); if (timetable_) { for (auto const& [tag, d] : timetable_->datasets_) { utl::verify(d.path_.starts_with("\n#") || fs::is_directory(d.path_) || fs::is_regular_file(d.path_), "timetable dataset {} does not exist: {}", tag, d.path_); utl::verify(!d.script_.has_value() || d.script_->starts_with("\nfunction") || fs::is_regular_file(*d.script_), "user script for {} not found at path: \"{}\"", tag, d.script_.value_or("")); if (d.clasz_bikes_allowed_.has_value()) { for (auto const& c : *d.clasz_bikes_allowed_) { nigiri::to_clasz(c.first); } } } if (timetable_->route_shapes_) { if (timetable_->route_shapes_->clasz_) { for (auto const& c : *timetable_->route_shapes_->clasz_) { nigiri::to_clasz(c.first); } } } } } bool config::requires_rt_timetable_updates() const { return timetable_.has_value() && ((has_elevators() && get_elevators()->url_.has_value()) || utl::any_of(timetable_->datasets_, [](auto&& d) { return d.second.rt_.has_value() && !d.second.rt_->empty(); })); } bool config::shapes_debug_api_enabled() const { return timetable_.has_value() && timetable_->route_shapes_.has_value() && timetable_->route_shapes_->debug_api_; } bool config::has_gbfs_feeds() const { return gbfs_.has_value() && !gbfs_->feeds_.empty(); } bool config::has_prima() const { return prima_.has_value(); } unsigned config::n_threads() const { return server_ .and_then([](config::server const& s) { return s.n_threads_ == 0U ? std::nullopt : std::optional{s.n_threads_}; }) .value_or(std::thread::hardware_concurrency()); } std::optional const& config::get_elevators() const { utl::verify(has_elevators(), "config::get_elevators() requires config::has_elevators()"); return std::get>(elevators_); } bool config::has_elevators() const { return std::visit( utl::overloaded{ [](std::optional const& x) { return x.has_value(); }, [](bool const x) { utl::verify(!x, "elevators=true is not supported"); return x; }}, elevators_); } std::optional config::get_street_routing() const { return std::visit( utl::overloaded{ [](std::optional const& x) { return x; }, [](bool const street_routing) { return street_routing ? std::optional{config::street_routing{}} : std::nullopt; }}, street_routing_); } bool config::use_street_routing() const { return std::visit( utl::overloaded{ [](std::optional const& o) { return o.has_value(); }, [](bool const b) { return b; }, }, street_routing_); } } // namespace motis ================================================ FILE: src/data.cc ================================================ #include "motis/data.h" #include #include #include "cista/io.h" #include "utl/get_or_create.h" #include "utl/read_file.h" #include "utl/verify.h" #include "adr/adr.h" #include "adr/cache.h" #include "adr/formatter.h" #include "adr/reverse.h" #include "adr/typeahead.h" #include "osr/elevation_storage.h" #include "osr/lookup.h" #include "osr/platforms.h" #include "osr/ways.h" #include "nigiri/routing/tb/tb_data.h" #include "nigiri/rt/create_rt_timetable.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/shapes_storage.h" #include "nigiri/timetable.h" #include "motis/config.h" #include "motis/constants.h" #include "motis/elevators/update_elevators.h" #include "motis/endpoints/initial.h" #include "motis/flex/flex_areas.h" #include "motis/hashes.h" #include "motis/match_platforms.h" #include "motis/metrics_registry.h" #include "motis/odm/bounds.h" #include "motis/point_rtree.h" #include "motis/railviz.h" #include "motis/tag_lookup.h" #include "motis/tiles_data.h" #include "motis/tt_location_rtree.h" namespace fs = std::filesystem; namespace n = nigiri; namespace motis { rt::rt() = default; rt::rt(ptr&& rtt, ptr&& e, ptr&& railviz) : rtt_{std::move(rtt)}, railviz_rt_{std::move(railviz)}, e_{std::move(e)} {} rt::~rt() = default; std::ostream& operator<<(std::ostream& out, data const& d) { return out << "\nt=" << d.t_.get() << "\nr=" << d.r_ << "\ntc=" << d.tc_ << "\nw=" << d.w_ << "\npl=" << d.pl_ << "\nl=" << d.l_ << "\ntt=" << d.tt_.get() << "\nlocation_rtee=" << d.location_rtree_ << "\nelevator_nodes=" << d.elevator_nodes_ << "\nmatches=" << d.matches_ << "\nrt=" << d.rt_ << "\n"; } data::data(std::filesystem::path p) : path_{std::move(p)}, config_{config::read(path_ / "config.yml")}, metrics_{std::make_unique()} {} data::data(std::filesystem::path p, config const& c) : path_{std::move(p)}, config_{c}, metrics_{std::make_unique()} { auto const verify_version = [&](bool cond, char const* name, auto&& ver) { if (!cond) { return; } auto const [key, expected_ver] = ver; auto const h = read_hashes(path_, name); auto const existing_ver_it = h.find(key); utl::verify(existing_ver_it != end(h), "{}: no existing version found [key={}], please re-run import; " "hashes: {}", name, key, to_str(h)); utl::verify(existing_ver_it->second == expected_ver, "{}: binary version mismatch [existing={} vs expected={}], " "please re-run import; hashes: {}", name, existing_ver_it->second, expected_ver, to_str(h)); }; verify_version(c.timetable_.has_value(), "tt", n_version()); verify_version(c.geocoding_ || c.reverse_geocoding_, "adr", adr_version()); verify_version(c.use_street_routing(), "osr", osr_version()); verify_version(c.use_street_routing() && c.timetable_, "matches", matches_version()); verify_version(c.tiles_.has_value(), "tiles", tiles_version()); verify_version(c.osr_footpath_, "osr_footpath", osr_footpath_version()); rt_ = std::make_shared(); if (c.prima_.has_value()) { if (c.prima_->bounds_.has_value()) { odm_bounds_ = std::make_unique(*c.prima_->bounds_); } if (c.prima_->ride_sharing_bounds_.has_value()) { ride_sharing_bounds_ = std::make_unique( *c.prima_->ride_sharing_bounds_); } } auto geocoder = std::async(std::launch::async, [&]() { f_ = std::make_unique(); if (c.geocoding_) { load_geocoder(); } if (c.reverse_geocoding_) { load_reverse_geocoder(); } }); auto tt = std::async(std::launch::async, [&]() { if (c.timetable_) { load_tt(config_.osr_footpath_ ? "tt_ext.bin" : "tt.bin"); if (c.timetable_->with_shapes_) { load_shapes(); } if (c.timetable_->railviz_) { load_railviz(); } if (c.timetable_->tb_) { load_tbd(); } for (auto const& [tag, d] : c.timetable_->datasets_) { if (d.rt_ && utl::any_of(*d.rt_, [](auto const& rt) { return rt.protocol_ == config::timetable::dataset::rt::protocol::auser || rt.protocol_ == config::timetable::dataset::rt::protocol::siri || rt.protocol_ == config::timetable::dataset::rt::protocol::siri_json; })) { load_auser_updater(tag, d); } } } }); auto street_routing = std::async(std::launch::async, [&]() { if (c.use_street_routing()) { load_osr(); } }); auto fa = std::async(std::launch::async, [&]() { if (c.timetable_ && c.use_street_routing()) { street_routing.wait(); tt.wait(); load_flex_areas(); } }); auto matches = std::async(std::launch::async, [&]() { if (c.use_street_routing() && c.timetable_) { load_matches(); load_way_matches(); } }); auto elevators = std::async(std::launch::async, [&]() { if (c.has_elevators()) { street_routing.wait(); elevator_osm_mapping_ = utl::visit( config_.elevators_, [](std::optional const& x) { return x; }) .and_then([](auto const& x) { return x.osm_mapping_; }) .transform([](std::string const& x) { return std::make_unique( x.starts_with("dhid,diid,osm_kind,osm_id") ? parse_elevator_id_osm_mapping(std::string_view{x}) : parse_elevator_id_osm_mapping(fs::path{x})); }) .value_or(nullptr); rt_->e_ = std::make_unique( *w_, elevator_osm_mapping_.get(), *elevator_nodes_, vector_map{}); if (c.get_elevators()->init_) { tt.wait(); auto new_rtt = std::make_unique( n::rt::create_rt_timetable(*tt_, rt_->rtt_->base_day_)); rt_->e_ = update_elevators( c, *this, c.get_elevators()->init_->starts_with("\n") ? std::string_view{*c.get_elevators()->init_} : cista::mmap{c.get_elevators()->init_->c_str(), cista::mmap::protection::READ} .view(), *new_rtt); rt_->rtt_ = std::move(new_rtt); } } }); auto tiles = std::async(std::launch::async, [&]() { if (c.tiles_) { load_tiles(); } }); auto const throw_if_failed = [](char const* context, auto& future) { try { future.get(); } catch (std::exception const& e) { throw utl::fail( "loading {} failed (if this happens after a fresh import, please " "file a bug report): {}", context, e.what()); } }; geocoder.wait(); tt.wait(); fa.wait(); street_routing.wait(); matches.wait(); elevators.wait(); tiles.wait(); throw_if_failed("geocoder", geocoder); throw_if_failed("tt", tt); throw_if_failed("street_routing", street_routing); throw_if_failed("matches", matches); throw_if_failed("elevators", elevators); throw_if_failed("tiles", tiles); initial_response_ = ep::get_initial_response(*this); utl_verify( shapes_ == nullptr || tt_ == nullptr || (tt_->n_routes() == shapes_->route_bboxes_.size() && tt_->n_routes() == shapes_->route_segment_bboxes_.size()), "mismatch: n_routes={}, n_route_bboxes={}, n_route_segment_bboxes={}", tt_->n_routes(), shapes_->route_bboxes_.size(), shapes_->route_segment_bboxes_.size()); utl_verify(matches_ == nullptr || tt_ == nullptr || matches_->size() == tt_->n_locations(), "mismatch: n_matches={}, n_locations={}", matches_->size(), tt_->n_locations()); } data::~data() = default; data::data(data&&) = default; data& data::operator=(data&&) = default; void data::load_osr() { auto const osr_path = path_ / "osr"; w_ = std::make_unique(osr_path, cista::mmap::protection::READ); l_ = std::make_unique(*w_, osr_path, cista::mmap::protection::READ); if (config_.get_street_routing()->elevation_data_dir_.has_value()) { elevations_ = osr::elevation_storage::try_open(osr_path); } elevator_nodes_ = std::make_unique>(get_elevator_nodes(*w_)); pl_ = std::make_unique(osr_path, cista::mmap::protection::READ); pl_->build_rtree(*w_); } void data::load_tt(fs::path const& p) { tags_ = tag_lookup::read(path_ / "tags.bin"); tt_ = n::timetable::read(path_ / p); tt_->resolve(); location_rtree_ = std::make_unique>( create_location_rtree(*tt_)); init_rtt(); } void data::load_flex_areas() { utl::verify(tt_ && w_ && l_, "flex areas requires tt={}, w={}, l={}", tt_ != nullptr, w_ != nullptr, l_ != nullptr); flex_areas_ = std::make_unique(*tt_, *w_, *l_); } void data::init_rtt(date::sys_days const d) { rt_->rtt_ = std::make_unique(n::rt::create_rt_timetable(*tt_, d)); } void data::load_shapes() { shapes_ = {}; shapes_ = std::make_unique( nigiri::shapes_storage{path_, cista::mmap::protection::READ}); } void data::load_railviz() { railviz_static_ = std::make_unique(*tt_, shapes_.get()); rt_->railviz_rt_ = std::make_unique(*tt_, *rt_->rtt_); } void data::load_tbd() { tbd_ = cista::read(path_ / "tbd.bin"); } void data::load_geocoder() { t_ = adr::read(path_ / "adr" / (config_.timetable_.has_value() ? "t_ext.bin" : "t.bin")); tc_ = std::make_unique(t_->strings_.size(), 100U); if (config_.timetable_.has_value()) { adr_ext_ = cista::read(path_ / "adr" / "location_extra_place.bin"); tz_ = std::make_unique< vector_map>(); auto cache = hash_map{}; for (auto const [type, areas] : utl::zip(t_->place_type_, t_->place_areas_)) { if (type != adr::amenity_category::kExtra) { continue; } auto const tz = t_->get_tz(areas); if (tz == adr::timezone_idx_t::invalid()) { tz_->push_back(nullptr); } else { auto const tz_name = t_->timezone_names_[tz].view(); tz_->push_back( utl::get_or_create(cache, tz_name, [&]() -> date::time_zone const* { try { return date::locate_zone(tz_name); } catch (...) { return nullptr; } })); } } } } void data::load_reverse_geocoder() { r_ = std::make_unique(path_ / "adr", cista::mmap::protection::READ); } void data::load_matches() { matches_ = cista::read(path_ / "matches.bin"); } void data::load_way_matches() { if (config_.timetable_.value().preprocess_max_matching_distance_ > 0.0) { way_matches_ = {}; way_matches_ = std::make_unique(way_matches_storage{ path_, cista::mmap::protection::READ, config_.timetable_.value().preprocess_max_matching_distance_}); } } void data::load_tiles() { auto const db_size = config_.tiles_.value().db_size_; tiles_ = std::make_unique( (path_ / "tiles" / "tiles.mdb").generic_string(), db_size); } void data::load_auser_updater(std::string_view tag, config::timetable::dataset const& d) { if (!auser_) { auser_ = std::make_unique>(); } auto const convert = [](config::timetable::dataset::rt::protocol const p) { switch (p) { case config::timetable::dataset::rt::protocol::auser: return n::rt::vdv_aus::updater::xml_format::kVdv; case config::timetable::dataset::rt::protocol::siri: return n::rt::vdv_aus::updater::xml_format::kSiri; case config::timetable::dataset::rt::protocol::siri_json: return n::rt::vdv_aus::updater::xml_format::kSiriJson; case config::timetable::dataset::rt::protocol::gtfsrt: std::unreachable(); } std::unreachable(); }; for (auto const& rt : *d.rt_) { auser_->try_emplace(rt.url_, *tt_, tags_->get_src(tag), convert(rt.protocol_)); } } } // namespace motis ================================================ FILE: src/direct_filter.cc ================================================ #include "motis/direct_filter.h" #include "utl/erase_if.h" #include "utl/visit.h" #include "nigiri/types.h" namespace motis { using namespace std::chrono_literals; namespace n = nigiri; void direct_filter(std::vector const& direct, std::vector& journeys) { auto const get_direct_duration = [&](auto const transport_mode_id) { auto const m = static_cast(transport_mode_id); auto const i = utl::find_if( direct, [&](auto const& d) { return d.legs_.front().mode_ == m; }); return i != end(direct) ? n::duration_t{std::chrono::round( std::chrono::seconds{i->duration_})} : n::duration_t::max(); }; auto const not_better_than_direct = [&](n::routing::journey const& j) { auto const first_leg_offset = utl::visit( j.legs_.front().uses_, [&](n::routing::offset const& o) { return std::optional{o}; }); auto const last_leg_offset = utl::visit( j.legs_.back().uses_, [&](n::routing::offset const& o) { return std::optional{o}; }); auto const longer_than_direct = [&](n::routing::offset const& o) { return std::optional{o.duration_ >= get_direct_duration(o.transport_mode_id_)}; }; return first_leg_offset.and_then(longer_than_direct).value_or(false) || last_leg_offset.and_then(longer_than_direct).value_or(false) || (first_leg_offset && last_leg_offset && first_leg_offset->transport_mode_id_ == last_leg_offset->transport_mode_id_ && first_leg_offset->duration_ + last_leg_offset->duration_ >= get_direct_duration(first_leg_offset->transport_mode_id_)); }; utl::erase_if(journeys, not_better_than_direct); } } // namespace motis ================================================ FILE: src/elevators/elevators.cc ================================================ #include "motis/elevators/elevators.h" #include "osr/ways.h" namespace motis { vector_map update_elevator_coordinates( osr::ways const& w, elevator_id_osm_mapping_t const* ids, hash_set const& elevator_nodes, vector_map&& elevators) { if (ids != nullptr) { auto id_to_elevator = hash_map{}; for (auto const [i, e] : utl::enumerate(elevators)) { if (e.id_str_.has_value()) { id_to_elevator.emplace(*e.id_str_, elevator_idx_t{i}); } } for (auto const n : elevator_nodes) { auto const id_it = ids->find(cista::to_idx(w.node_to_osm_[n])); if (id_it != end(*ids)) { auto const e_it = id_to_elevator.find(id_it->second); if (e_it != end(id_to_elevator)) { elevators[e_it->second].pos_ = w.get_node_pos(n).as_latlng(); } } } } return elevators; } elevators::elevators(osr::ways const& w, elevator_id_osm_mapping_t const* ids, hash_set const& elevator_nodes, vector_map&& elevators) : elevators_{update_elevator_coordinates( w, ids, elevator_nodes, std::move(elevators))}, elevators_rtree_{create_elevator_rtree(elevators_)}, blocked_{get_blocked_elevators( w, ids, elevators_, elevators_rtree_, elevator_nodes)} {} } // namespace motis ================================================ FILE: src/elevators/match_elevators.cc ================================================ #include "motis/elevators/match_elevator.h" #include "utl/enumerate.h" #include "utl/parallel_for.h" #include "osr/ways.h" namespace motis { point_rtree create_elevator_rtree( vector_map const& elevators) { auto t = point_rtree{}; for (auto const [i, e] : utl::enumerate(elevators)) { t.add(e.pos_, elevator_idx_t{i}); } return t; } osr::hash_set get_elevator_nodes(osr::ways const& w) { auto nodes = osr::hash_set{}; for (auto way = osr::way_idx_t{0U}; way != w.n_ways(); ++way) { for (auto const n : w.r_->way_nodes_[way]) { if (w.r_->node_properties_[n].is_elevator()) { nodes.emplace(n); } } } return nodes; } elevator_idx_t match_elevator( point_rtree const& rtree, vector_map const& elevators, osr::ways const& w, osr::node_idx_t const n) { auto const pos = w.get_node_pos(n).as_latlng(); auto closest = elevator_idx_t::invalid(); auto closest_dist = std::numeric_limits::max(); rtree.find(geo::box{pos, 20.0}, [&](elevator_idx_t const e) { auto const dist = geo::distance(elevators[e].pos_, pos); if (dist < 20 && dist < closest_dist) { closest_dist = dist; closest = e; } }); return closest; } osr::bitvec get_blocked_elevators( osr::ways const& w, elevator_id_osm_mapping_t const* ids, vector_map const& elevators, point_rtree const& elevators_rtree, osr::hash_set const& elevator_nodes) { auto inactive = osr::hash_set{}; auto inactive_mutex = std::mutex{}; auto id_to_elevator = hash_map{}; for (auto const [i, e] : utl::enumerate(elevators)) { if (e.id_str_.has_value()) { id_to_elevator.emplace(*e.id_str_, elevator_idx_t{i}); } } utl::parallel_for(elevator_nodes, [&](osr::node_idx_t const n) { auto e = elevator_idx_t::invalid(); // SIRI-FM matching by DIID: // node idx -> OSM ID -> DIID -> elevator_idx if (ids != nullptr) { auto const id_it = ids->find(to_idx(w.node_to_osm_[n])); if (id_it != end(*ids)) { auto const e_it = id_to_elevator.find(id_it->second); if (e_it != end(id_to_elevator)) { e = e_it->second; } } } // DB FaSta API: geomatching if (e == elevator_idx_t::invalid()) { e = match_elevator(elevators_rtree, elevators, w, n); } if (e != elevator_idx_t::invalid() && !elevators[e].status_) { auto const lock = std::scoped_lock{inactive_mutex}; inactive.emplace(n); } }); auto blocked = osr::bitvec{}; blocked.resize(w.n_nodes()); for (auto const n : inactive) { blocked.set(n, true); } return blocked; } } // namespace motis ================================================ FILE: src/elevators/parse_elevator_id_osm_mapping.cc ================================================ #include "motis/elevators/parse_elevator_id_osm_mapping.h" #include "utl/parser/csv_range.h" namespace motis { elevator_id_osm_mapping_t parse_elevator_id_osm_mapping(std::string_view s) { struct row { utl::csv_col dhid_; utl::csv_col diid_; utl::csv_col osm_kind_; utl::csv_col osm_id_; }; auto map = elevator_id_osm_mapping_t{}; utl::for_each_row(s, [&](row const& r) { map.emplace(*r.osm_id_, std::string{r.diid_->view()}); }); return map; } elevator_id_osm_mapping_t parse_elevator_id_osm_mapping( std::filesystem::path const& p) { return parse_elevator_id_osm_mapping( cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ} .view()); } } // namespace motis ================================================ FILE: src/elevators/parse_fasta.cc ================================================ #include "motis/elevators/parse_fasta.h" #include #include "boost/json.hpp" #include "date/date.h" #include "utl/enumerate.h" namespace n = nigiri; namespace json = boost::json; namespace motis { n::unixtime_t parse_date_time(std::string_view s) { auto t = n::unixtime_t{}; auto ss = std::stringstream{}; ss.exceptions(std::ios_base::failbit | std::ios_base::badbit); ss << s; ss >> date::parse("%FT%T", t); return t; } std::vector> parse_out_of_service( json::object const& o) { auto ret = std::vector>{}; if (!o.contains("outOfService")) { return ret; } for (auto const& entry : o.at("outOfService").as_array()) { auto const& interval = entry.as_array(); if (interval.size() != 2U || !(interval[0].is_string() && interval[1].is_string())) { fmt::println("skip: unable to parse out of service interval {}", json::serialize(entry)); continue; } ret.emplace_back(n::interval{parse_date_time(interval[0].as_string()), parse_date_time(interval[1].as_string())}); } return ret; } std::optional parse_elevator(json::value const& e) { if (e.at("type") != "ELEVATOR") { return std::nullopt; } try { auto const& o = e.as_object(); if (!o.contains("geocoordY") || !o.contains("geocoordX") || !o.contains("state")) { std::cout << "skip: missing attributes: " << o << "\n"; return std::nullopt; } auto const id = o.contains("equipmentnumber") ? e.at("equipmentnumber").to_number() : 0U; return elevator{id, std::nullopt, geo::latlng{e.at("geocoordY").to_number(), e.at("geocoordX").to_number()}, e.at("state").as_string() != "INACTIVE", o.contains("description") ? std::string{o.at("description").as_string()} : "", parse_out_of_service(o)}; } catch (std::exception const& ex) { std::cout << "error on value: " << e << ": " << ex.what() << "\n"; return std::nullopt; } } vector_map parse_fasta(std::string_view s) { auto ret = vector_map{}; for (auto const [i, e] : utl::enumerate(json::parse(s).as_array())) { if (auto x = parse_elevator(e); x.has_value()) { ret.emplace_back(std::move(*x)); } } return ret; } vector_map parse_fasta( std::filesystem::path const& p) { return parse_fasta( cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ} .view()); } } // namespace motis ================================================ FILE: src/elevators/parse_siri_fm.cc ================================================ #include "motis/elevators/parse_siri_fm.h" #include "pugixml.hpp" namespace motis { std::optional parse_facility_condition(pugi::xml_node const& fc) { auto const id = fc.child_value("FacilityRef"); if (id == nullptr || *id == '\0') { return std::nullopt; } auto const status = fc.child("FacilityStatus").child_value("Status"); if (status == nullptr || *status == '\0') { return std::nullopt; } return elevator{ .id_ = 0U, .id_str_ = std::string{id}, .pos_ = geo::latlng{}, .status_ = std::string_view{status} == "available", .desc_ = "", .out_of_service_ = {}, }; } vector_map parse_siri_fm(std::string_view s) { auto doc = pugi::xml_document{}; if (!doc.load_buffer(s.data(), s.size())) { return {}; } auto ret = vector_map{}; for (auto fc : doc.child("Siri") .child("ServiceDelivery") .child("FacilityMonitoringDelivery") .children("FacilityCondition")) { if (auto e = parse_facility_condition(fc); e.has_value()) { ret.emplace_back(std::move(*e)); } } return ret; } vector_map parse_siri_fm( std::filesystem::path const& p) { return parse_siri_fm( cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ} .view()); } } // namespace motis ================================================ FILE: src/elevators/update_elevators.cc ================================================ #include "motis/elevators/update_elevators.h" #include "utl/verify.h" #include "nigiri/logging.h" #include "motis/config.h" #include "motis/constants.h" #include "motis/data.h" #include "motis/elevators/elevators.h" #include "motis/elevators/parse_fasta.h" #include "motis/elevators/parse_siri_fm.h" #include "motis/update_rtt_td_footpaths.h" namespace n = nigiri; namespace motis { using elevator_map_t = hash_map; elevator_map_t to_map(vector_map const& elevators) { auto m = elevator_map_t{}; for (auto const [i, e] : utl::enumerate(elevators)) { m.emplace(e.id_, elevator_idx_t{i}); } return m; } ptr update_elevators(config const& c, data const& d, std::string_view body, n::rt_timetable& new_rtt) { auto new_e = std::make_unique( *d.w_, d.elevator_osm_mapping_.get(), *d.elevator_nodes_, body.contains("e_; auto const old_map = to_map(old_e.elevators_); auto const new_map = to_map(new_e->elevators_); auto tasks = hash_set>{}; auto const add_tasks = [&](std::optional const& pos) { if (!pos.has_value()) { return; } d.location_rtree_->in_radius(*pos, kElevatorUpdateRadius, [&](n::location_idx_t const l) { tasks.emplace(l, osr::direction::kForward); tasks.emplace(l, osr::direction::kBackward); }); }; for (auto const& [id, e_idx] : old_map) { auto const it = new_map.find(id); if (it == end(new_map)) { // Elevator got removed. // Not listed in new => default status = ACTIVE // Update if INACTIVE before (= status changed) if (old_e.elevators_[e_idx].status_ == false) { add_tasks(old_e.elevators_[e_idx].pos_); } } else { // Elevator remained. Update if status changed. if (new_e->elevators_[it->second].status_ != old_e.elevators_[e_idx].status_) { add_tasks(new_e->elevators_[it->second].pos_); } } } for (auto const& [id, e_idx] : new_map) { auto const it = old_map.find(id); if (it == end(old_map) && new_e->elevators_[e_idx].status_ == false) { // New elevator not seen before, elevator is NOT working. Update. add_tasks(new_e->elevators_[e_idx].pos_); } } n::log(n::log_lvl::info, "motis.rt.elevators", "elevator update: {} routing tasks", tasks.size()); update_rtt_td_footpaths( *d.w_, *d.l_, *d.pl_, *d.tt_, *d.location_rtree_, *new_e, *d.matches_, tasks, d.rt_->rtt_.get(), new_rtt, std::chrono::seconds{c.timetable_.value().max_footpath_length_ * 60}); return new_e; } } // namespace motis ================================================ FILE: src/endpoints/adr/filter_conv.cc ================================================ #include "motis/endpoints/adr/filter_conv.h" namespace a = adr; namespace motis { adr::filter_type to_filter_type( std::optional const& f) { if (f.has_value()) { switch (*f) { case api::LocationTypeEnum::ADDRESS: return a::filter_type::kAddress; case api::LocationTypeEnum::PLACE: return a::filter_type::kPlace; case api::LocationTypeEnum::STOP: return a::filter_type::kExtra; } } return a::filter_type::kNone; } } // namespace motis ================================================ FILE: src/endpoints/adr/geocode.cc ================================================ #include "motis/endpoints/adr/geocode.h" #include "boost/thread/tss.hpp" #include "utl/for_each_bit_set.h" #include "utl/to_vec.h" #include "fmt/format.h" #include "net/bad_request_exception.h" #include "nigiri/timetable.h" #include "adr/adr.h" #include "adr/typeahead.h" #include "motis/config.h" #include "motis/endpoints/adr/filter_conv.h" #include "motis/endpoints/adr/suggestions_to_response.h" #include "motis/parse_location.h" #include "motis/timetable/modes_to_clasz_mask.h" namespace n = nigiri; namespace a = adr; namespace motis::ep { constexpr auto const kDefaultSuggestions = 10U; a::guess_context& get_guess_context(a::typeahead const& t, a::cache& cache) { auto static ctx = boost::thread_specific_ptr{}; if (ctx.get() == nullptr || &ctx.get()->cache_ != &cache) { ctx.reset(new a::guess_context{cache}); } ctx->resize(t); return *ctx; } api::geocode_response geocode::operator()( boost::urls::url_view const& url) const { auto const params = api::geocode_params{url.params()}; auto const place = params.place_.and_then([](std::string const& s) { auto const parsed = parse_location(s); utl::verify(parsed.has_value(), "could not parse place {}", s); return std::optional{parsed.value().pos_}; }); auto const required_modes = params.mode_.transform([](std::vector const& modes) { return to_clasz_mask(modes); }); auto& ctx = get_guess_context(t_, cache_); auto lang_indices = basic_string{{a::kDefaultLang}}; if (params.language_.has_value()) { for (auto const& language : *params.language_) { auto const l_idx = t_.resolve_language(language); if (l_idx != a::language_idx_t::invalid()) { lang_indices.push_back(l_idx); } } } auto const place_filter = required_modes .and_then( [&](n::routing::clasz_mask_t const required_clasz) -> std::optional> { if (required_clasz == 0U) { return std::nullopt; } return {std::function{ [&, required_clasz](adr::place_idx_t place_idx) { if (t_.place_type_[place_idx] != adr::amenity_category::kExtra) { return true; } auto const i = adr_extra_place_idx_t{ static_cast( place_idx - t_.ext_start_)}; return (ae_->place_clasz_.at(i) & required_clasz) != 0U; }}}; }) .value_or(std::function{}); auto const config_limit = config_.get_limits().geocode_max_suggestions_; auto const requested_limit = params.numResults_.value_or(kDefaultSuggestions); utl::verify(requested_limit >= 1, "limit must be >= 1"); utl::verify( requested_limit <= config_limit, "limit must be <= geocode_max_suggestions ({})", config_limit); auto const token_pos = a::get_suggestions( t_, params.text_, static_cast(requested_limit), lang_indices, ctx, place, static_cast(params.placeBias_), to_filter_type(params.type_), place_filter); return suggestions_to_response(t_, f_, ae_, tt_, tags_, w_, pl_, matches_, lang_indices, token_pos, ctx.suggestions_); } } // namespace motis::ep ================================================ FILE: src/endpoints/adr/reverse_geocode.cc ================================================ #include "motis/endpoints/adr/reverse_geocode.h" #include "net/bad_request_exception.h" #include "adr/guess_context.h" #include "adr/reverse.h" #include "motis/config.h" #include "motis/endpoints/adr/filter_conv.h" #include "motis/endpoints/adr/suggestions_to_response.h" #include "motis/parse_location.h" namespace a = adr; namespace motis::ep { constexpr auto const kDefaultResults = 5U; api::reverseGeocode_response reverse_geocode::operator()( boost::urls::url_view const& url) const { auto const params = api::reverseGeocode_params{url.params()}; auto const config_limit = config_.get_limits().reverse_geocode_max_results_; auto const requested_limit = params.numResults_.value_or(kDefaultResults); utl::verify(requested_limit >= 1, "limit must be >= 1"); utl::verify( requested_limit <= config_limit, "limit must be <= reverse_geocode_max_results ({})", config_limit); return suggestions_to_response( t_, f_, ae_, tt_, tags_, w_, pl_, matches_, {}, {}, r_.lookup(t_, parse_location((params.place_))->pos_, static_cast(requested_limit), to_filter_type(params.type_))); } } // namespace motis::ep ================================================ FILE: src/endpoints/adr/suggestions_to_response.cc ================================================ #include "motis/endpoints/adr/suggestions_to_response.h" #include "utl/for_each_bit_set.h" #include "utl/helpers/algorithm.h" #include "utl/overloaded.h" #include "utl/to_vec.h" #include "utl/visit.h" #include "nigiri/timetable.h" #include "adr/typeahead.h" #include "motis/journey_to_response.h" #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" namespace a = adr; namespace n = nigiri; namespace motis { long get_area_lang_idx(a::typeahead const& t, a::language_list_t const& languages, a::area_idx_t const a) { for (auto i = 0U; i != languages.size(); ++i) { auto const j = languages.size() - i - 1U; auto const lang_idx = a::find_lang(t.area_name_lang_[a], languages[j]); if (lang_idx != -1) { return lang_idx; } } return -1; } api::geocode_response suggestions_to_response( adr::typeahead const& t, adr::formatter const& f, adr_ext const* ae, n::timetable const* tt, tag_lookup const* tags, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, basic_string const& lang_indices, std::vector const& token_pos, std::vector const& suggestions) { return utl::to_vec(suggestions, [&](a::suggestion const& s) { auto const areas = t.area_sets_[s.area_set_]; auto modes = std::optional>{}; auto importance = std::optional{}; auto type = api::LocationTypeEnum{}; auto street = std::optional{}; auto house_number = std::optional{}; auto id = std::string{}; auto level = std::optional{}; auto category = std::optional{}; utl::visit( s.location_, [&](a::place_idx_t const p) { type = t.place_type_[p] == a::amenity_category::kExtra ? api::LocationTypeEnum::STOP : api::LocationTypeEnum::PLACE; if (type == api::LocationTypeEnum::STOP) { if (tt != nullptr && tags != nullptr) { auto const l = n::location_idx_t{ static_cast(s.get_osm_id(t))}; level = get_level(w, pl, matches, l); id = tags->id(*tt, l); } else { id = fmt::format("stop/{}", p); } if (ae != nullptr) { auto const i = adr_extra_place_idx_t{ static_cast(p - t.ext_start_)}; modes = to_modes(ae->place_clasz_[i], 5); importance = ae->place_importance_[i]; } } else { category = to_str(t.place_type_[p]); id = fmt::format("{}/{}", t.place_is_way_[to_idx(p)] ? "way" : "node", t.place_osm_ids_[p]); } return std::string{t.strings_[s.str_].view()}; }, [&](a::address const addr) { type = api::LocationTypeEnum::ADDRESS; if (addr.house_number_ != a::address::kNoHouseNumber) { street = t.strings_[s.str_].view(); house_number = t.strings_[t.house_numbers_[addr.street_][addr.house_number_]] .view(); return fmt::format("{} {}", *street, *house_number); } else { return std::string{t.strings_[s.str_].view()}; } }); auto tokens = std::vector>{}; utl::for_each_set_bit(s.matched_tokens_, [&](auto const i) { assert(i < token_pos.size()); tokens.emplace_back( std::vector{static_cast(token_pos[i].start_idx_), static_cast(token_pos[i].size_)}); }); auto const is_matched = [&](std::size_t const i) { return (((1U << i) & s.matched_areas_) != 0U); }; auto api_areas = std::vector{}; for (auto const [i, a] : utl::enumerate(areas)) { auto const admin_lvl = t.area_admin_level_[a]; if (admin_lvl == a::kPostalCodeAdminLevel || admin_lvl == a::kTimezoneAdminLevel) { continue; } auto const language = is_matched(i) ? s.matched_area_lang_[i] : get_area_lang_idx(t, lang_indices, a); auto const area_name = t.strings_[t.area_names_[a][language == -1 ? a::kDefaultLangIdx : static_cast(language)]] .view(); api_areas.emplace_back(api::Area{ .name_ = std::string{area_name}, .adminLevel_ = static_cast(to_idx(admin_lvl)), .matched_ = is_matched(i), .unique_ = s.unique_area_idx_.has_value() && *s.unique_area_idx_ == i, .default_ = s.city_area_idx_.has_value() && *s.city_area_idx_ == i}); } auto const country_code = s.get_country_code(t); return api::Match{ .type_ = type, .category_ = std::move(category), .tokens_ = std::move(tokens), .name_ = s.format(t, f, country_code.value_or("DE")), .id_ = std::move(id), .lat_ = s.coordinates_.as_latlng().lat_, .lon_ = s.coordinates_.as_latlng().lng_, .level_ = level, .street_ = std::move(street), .houseNumber_ = std::move(house_number), .country_ = country_code.and_then( [](std::string_view s) { return std::optional{std::string{s}}; }), .zip_ = s.zip_area_idx_.and_then([&](unsigned const zip_area_idx) { return std::optional{ t.strings_[t.area_names_[areas[zip_area_idx]][a::kDefaultLangIdx]] .view()}; }), .tz_ = s.tz_ == a::timezone_idx_t::invalid() ? std::nullopt : std::optional{std::string{t.timezone_names_[s.tz_].view()}}, .areas_ = std::move(api_areas), .score_ = s.score_, .modes_ = std::move(modes), .importance_ = importance}; }); } } // namespace motis ================================================ FILE: src/endpoints/elevators.cc ================================================ #include "motis/endpoints/elevators.h" #include "net/too_many_exception.h" #include "osr/geojson.h" #include "boost/json.hpp" #include "fmt/chrono.h" #include "fmt/format.h" #include "motis/data.h" #include "motis/elevators/match_elevator.h" namespace json = boost::json; namespace n = nigiri; namespace std { n::unixtime_t tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) { auto x = n::unixtime_t{}; auto ss = std::stringstream{std::string{jv.as_string()}}; ss >> date::parse("%FT%T", x); return x; } void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, n::unixtime_t const& v) { auto ss = std::stringstream{}; ss << date::format("%FT%TZ", v); jv = json::string{ss.str()}; } } // namespace std namespace nigiri { template n::interval tag_invoke(boost::json::value_to_tag>, boost::json::value const& jv) { auto x = n::interval{}; x.from_ = json::value_to(jv.as_array().at(0)); x.to_ = json::value_to(jv.as_array().at(1)); return x; } template void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, n::interval const& v) { auto& a = (jv = boost::json::array{}).as_array(); a.emplace_back(json::value_from(v.from_)); a.emplace_back(json::value_from(v.to_)); } } // namespace nigiri namespace motis::ep { constexpr auto const kLimit = 4096U; json::value elevators::operator()(json::value const& query) const { auto const rt = std::atomic_load(&rt_); auto const e = rt->e_.get(); auto matches = json::array{}; if (e == nullptr) { return json::value{{"type", "FeatureCollection"}, {"features", matches}}; } auto const& q = query.as_array(); auto const min = geo::latlng{q[1].as_double(), q[0].as_double()}; auto const max = geo::latlng{q[3].as_double(), q[2].as_double()}; e->elevators_rtree_.find(geo::box{min, max}, [&](elevator_idx_t const i) { utl::verify(matches.size() < kLimit, "too many elevators"); auto const& x = e->elevators_[i]; matches.emplace_back(json::value{ {"type", "Feature"}, {"properties", {{"type", "api"}, {"id", x.id_}, {"desc", x.desc_}, {"status", (x.status_ ? "ACTIVE" : "INACTIVE")}, {"outOfService", json::value_from(x.out_of_service_)}}}, {"geometry", osr::to_point(osr::point::from_latlng(x.pos_))}}); }); for (auto const n : l_.find_elevators({min, max})) { auto const match = match_elevator(e->elevators_rtree_, e->elevators_, w_, n); auto const pos = w_.get_node_pos(n); if (match != elevator_idx_t::invalid()) { auto const& x = e->elevators_[match]; utl::verify(matches.size() < kLimit, "too many elevators"); matches.emplace_back(json::value{ {"type", "Feature"}, {"properties", {{"type", "match"}, {"osm_node_id", to_idx(w_.node_to_osm_[n])}, {"id", x.id_}, {"desc", x.desc_}, {"status", x.status_ ? "ACTIVE" : "INACTIVE"}, {"outOfService", json::value_from(x.out_of_service_)}}}, {"geometry", osr::to_line_string({pos, osr::point::from_latlng(x.pos_)})}}); } } return json::value{{"type", "FeatureCollection"}, {"features", matches}}; } } // namespace motis::ep ================================================ FILE: src/endpoints/graph.cc ================================================ #include "motis/endpoints/graph.h" #include "net/too_many_exception.h" #include "osr/geojson.h" #include "osr/routing/profiles/car_sharing.h" #include "osr/routing/route.h" namespace json = boost::json; namespace motis::ep { constexpr auto const kMaxWays = 2048U; json::value graph::operator()(json::value const& query) const { auto const& q = query.as_object(); auto const& x = query.at("waypoints").as_array(); auto const min = geo::latlng{x[1].as_double(), x[0].as_double()}; auto const max = geo::latlng{x[3].as_double(), x[2].as_double()}; auto const level = q.contains("level") ? osr::level_t{q.at("level").to_number()} : osr::kNoLevel; auto gj = osr::geojson_writer{.w_ = w_}; auto n_ways = 0U; l_.find({min, max}, [&](osr::way_idx_t const w) { if (++n_ways == kMaxWays) { throw utl::fail("too many ways"); } if (level == osr::kNoLevel) { gj.write_way(w); return; } auto const way_prop = w_.r_->way_properties_[w]; if (way_prop.is_elevator()) { auto const n = w_.r_->way_nodes_[w][0]; auto const np = w_.r_->node_properties_[n]; if (np.is_multi_level()) { auto has_level = false; utl::for_each_set_bit( osr::foot::get_elevator_multi_levels(*w_.r_, n), [&](auto&& bit) { has_level |= (level == osr::level_t{static_cast(bit)}); }); if (has_level) { gj.write_way(w); return; } } } if ((level == osr::level_t{0.F} && way_prop.from_level() == osr::kNoLevel) || way_prop.from_level() == level || way_prop.to_level() == level) { gj.write_way(w); return; } }); gj.finish(&osr::get_dijkstra>()); return gj.json(); } } // namespace motis::ep ================================================ FILE: src/endpoints/gtfsrt.cc ================================================ #include "motis/endpoints/gtfsrt.h" #include #ifdef NO_DATA #undef NO_DATA #endif #include "gtfsrt/gtfs-realtime.pb.h" #include "utl/enumerate.h" #include "net/too_many_exception.h" #include "nigiri/loader/gtfs/stop_seq_number_encoding.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/timetable.h" #include "nigiri/types.h" #include "motis/data.h" #include "motis/tag_lookup.h" namespace n = nigiri; namespace gtfsrt = transit_realtime; namespace protob = google::protobuf; namespace motis::ep { void add_trip_updates(n::timetable const& tt, tag_lookup const& tags, nigiri::rt::frun const& fr, transit_realtime::FeedMessage& fm) { fr.for_each_trip([&](n::trip_idx_t trip_idx, n::interval const subrange) { auto fe = fm.add_entity(); auto const trip_id = tags.id_fragments( tt, fr[subrange.from_ - fr.stop_range_.from_], n::event_type::kDep); fe->set_id(trip_id.trip_id_); auto tu = fe->mutable_trip_update(); auto td = tu->mutable_trip(); td->set_trip_id(trip_id.trip_id_); td->set_start_time(fmt::format("{}:00", trip_id.start_time_)); td->set_start_date(trip_id.start_date_); td->set_schedule_relationship( fr.is_cancelled() ? transit_realtime::TripDescriptor_ScheduleRelationship:: TripDescriptor_ScheduleRelationship_CANCELED : !fr.is_scheduled() ? transit_realtime::TripDescriptor_ScheduleRelationship:: TripDescriptor_ScheduleRelationship_ADDED : transit_realtime::TripDescriptor_ScheduleRelationship:: TripDescriptor_ScheduleRelationship_SCHEDULED); if (!fr.is_scheduled()) { auto const route_id_idx = fr.rtt_->rt_transport_route_id_.at(fr.rt_); if (route_id_idx != n::route_id_idx_t::invalid()) { td->set_route_id( tt.route_ids_[fr.rtt_->rt_transport_src_.at(fr.rt_)].ids_.get( route_id_idx)); } } if (fr.is_cancelled()) { return; } auto const seq_numbers = fr.is_scheduled() ? n::loader::gtfs:: stop_seq_number_range{{tt.trip_stop_seq_numbers_[trip_idx]}, static_cast( subrange.size())} : n::loader::gtfs::stop_seq_number_range{ std::span{}, static_cast(fr.size())}; auto stop_idx = fr.is_scheduled() ? subrange.from_ : static_cast(0U); auto seq_it = begin(seq_numbers); auto last_delay = n::duration_t::max(); for (; seq_it != end(seq_numbers); ++stop_idx, ++seq_it) { auto const s = fr[stop_idx - fr.stop_range_.from_]; transit_realtime::TripUpdate_StopTimeUpdate* stu = nullptr; auto const set_stu = [&]() { if (stu != nullptr) { return; } stu = tu->add_stop_time_update(); stu->set_stop_id( tt.locations_.ids_[s.get_stop().location_idx()].view()); stu->set_stop_sequence(*seq_it); }; auto const to_unix = [&](n::unixtime_t t) { return std::chrono::time_point_cast(t) .time_since_epoch() .count(); }; auto const to_delay_seconds = [&](n::duration_t t) { return static_cast( std::chrono::duration_cast(t).count()); }; if (s.stop_idx_ != 0) { auto const arr_delay = s.delay(nigiri::event_type::kArr); if (arr_delay != last_delay || !fr.is_scheduled()) { set_stu(); auto ar = stu->mutable_arrival(); ar->set_time(to_unix(s.time(nigiri::event_type::kArr))); ar->set_delay(to_delay_seconds(arr_delay)); last_delay = arr_delay; } } if (s.stop_idx_ != fr.size() - 1) { auto const dep_delay = s.delay(nigiri::event_type::kDep); if (dep_delay != last_delay || !fr.is_scheduled()) { set_stu(); auto dep = stu->mutable_departure(); dep->set_time(to_unix(s.time(nigiri::event_type::kDep))); dep->set_delay(to_delay_seconds(dep_delay)); last_delay = dep_delay; } } if (s.is_cancelled() && !s.get_scheduled_stop().is_cancelled()) { set_stu(); stu->set_schedule_relationship( transit_realtime::TripUpdate_StopTimeUpdate_ScheduleRelationship:: TripUpdate_StopTimeUpdate_ScheduleRelationship_SKIPPED); } } }); } void add_rt_transports(n::timetable const& tt, tag_lookup const& tags, n::rt_timetable const& rtt, transit_realtime::FeedMessage& fm) { for (auto rt_t = nigiri::rt_transport_idx_t{0}; rt_t < rtt.n_rt_transports(); ++rt_t) { auto const fr = n::rt::frun::from_rt(tt, &rtt, rt_t); add_trip_updates(tt, tags, fr, fm); } } void add_cancelled_transports(n::timetable const& tt, tag_lookup const& tags, n::rt_timetable const& rtt, transit_realtime::FeedMessage& fm) { auto const start_time = std::max( std::chrono::time_point_cast( std::chrono::system_clock::now() - std::chrono::duration_cast(n::kTimetableOffset)), tt.internal_interval().from_); auto const end_time = std::min( std::chrono::time_point_cast( start_time + std::chrono::duration_cast(std::chrono::days{6})), tt.internal_interval().to_); auto const [start_day, _] = tt.day_idx_mam(start_time); auto const [end_day, _1] = tt.day_idx_mam(end_time); for (auto r = nigiri::route_idx_t{0}; r < tt.n_routes(); ++r) { for (auto const [i, t_idx] : utl::enumerate(tt.route_transport_ranges_[r])) { for (auto day = start_day; day <= end_day; ++day) { auto const t = n::transport{t_idx, day}; auto const is_cancelled = tt.bitfields_[tt.transport_traffic_days_[t.t_idx_]].test( to_idx(t.day_)) && !rtt.bitfields_[rtt.transport_traffic_days_[t.t_idx_]].test( to_idx(t.day_)); if (!is_cancelled) { continue; } auto fr = n::rt::frun::from_t(tt, &rtt, t); if (fr.is_rt()) { continue; } add_trip_updates(tt, tags, fr, fm); } } } } net::reply gtfsrt::operator()(net::route_request const& req, bool) const { utl::verify(tt_ != nullptr && tags_ != nullptr, "no tt initialized"); auto const rt = std::atomic_load(&rt_); auto const rtt = rt->rtt_.get(); utl::verify( config_.get_limits().gtfsrt_expose_max_trip_updates_ != 0 && rtt->n_rt_transports() < config_.get_limits().gtfsrt_expose_max_trip_updates_, "number of trip updates above configured limit"); auto fm = transit_realtime::FeedMessage(); auto fh = fm.mutable_header(); fh->set_gtfs_realtime_version("2.0"); fh->set_incrementality( transit_realtime::FeedHeader_Incrementality_FULL_DATASET); auto const time = std::time(nullptr); fh->set_timestamp(static_cast(time)); if (rtt != nullptr) { add_rt_transports(*tt_, *tags_, *rtt, fm); add_cancelled_transports(*tt_, *tags_, *rtt, fm); } auto res = net::web_server::string_res_t{boost::beast::http::status::ok, req.version()}; res.insert(boost::beast::http::field::content_type, "application/x-protobuf"); res.keep_alive(req.keep_alive()); set_response_body(res, req, fm.SerializeAsString()); return res; } } // namespace motis::ep ================================================ FILE: src/endpoints/initial.cc ================================================ #include "motis/endpoints/initial.h" #include "motis/config.h" #include "utl/erase_if.h" #include "utl/to_vec.h" #include "tiles/fixed/convert.h" #include "tiles/fixed/fixed_geometry.h" #include "nigiri/timetable.h" #include "motis/data.h" namespace n = nigiri; namespace motis::ep { api::initial_response get_initial_response(data const& d) { auto const get_quantiles = [](std::vector&& coords) { utl::erase_if(coords, [](auto const c) { return c == 0.; }); if (coords.empty()) { return std::make_pair(0., 0.); } if (coords.size() < 10) { return std::make_pair(coords.front(), coords.back()); } std::sort(begin(coords), end(coords)); constexpr auto const kQuantile = .8; return std::make_pair( coords.at(static_cast(coords.size()) * (1 - kQuantile)), coords.at(static_cast(coords.size()) * (kQuantile))); }; auto zoom = 0U; auto center = geo::latlng{}; auto const tt = d.tt_.get(); if (tt != nullptr) { auto const [lat_min, lat_max] = get_quantiles(utl::to_vec( tt->locations_.coordinates_, [](auto const& s) { return s.lat_; })); auto const [lng_min, lng_max] = get_quantiles(utl::to_vec( tt->locations_.coordinates_, [](auto const& s) { return s.lng_; })); auto const fixed0 = tiles::latlng_to_fixed({lat_min, lng_min}); auto const fixed1 = tiles::latlng_to_fixed({lat_max, lng_max}); center = tiles::fixed_to_latlng( {(fixed0.x() + fixed1.x()) / 2, (fixed0.y() + fixed1.y()) / 2}); auto const span = static_cast(std::max( std::abs(fixed0.x() - fixed1.x()), std::abs(fixed0.y() - fixed1.y()))); for (; zoom < (tiles::kMaxZoomLevel - 1); ++zoom) { if (((tiles::kTileSize * 2ULL) * (1ULL << (tiles::kMaxZoomLevel - (zoom + 1)))) < span) { break; } } } auto const limits = d.config_.get_limits(); return { .lat_ = center.lat_, .lon_ = center.lng_, .zoom_ = static_cast(zoom), .serverConfig_ = api::ServerConfig{ .motisVersion_ = std::string{d.motis_version_}, .hasElevation_ = d.config_.get_street_routing() .transform([](config::street_routing const& x) { return x.elevation_data_dir_.has_value(); }) .value_or(false), .hasRoutedTransfers_ = d.config_.osr_footpath_, .hasStreetRouting_ = d.config_.get_street_routing().has_value(), .maxOneToManySize_ = static_cast(limits.onetomany_max_many_), .maxOneToAllTravelTimeLimit_ = static_cast(limits.onetoall_max_travel_minutes_), .maxPrePostTransitTimeLimit_ = static_cast( limits.street_routing_max_prepost_transit_seconds_), .maxDirectTimeLimit_ = static_cast(limits.street_routing_max_direct_seconds_), .shapesDebugEnabled_ = d.config_.shapes_debug_api_enabled()}}; } api::initial_response initial::operator()(boost::urls::url_view const&) const { return response_; } } // namespace motis::ep ================================================ FILE: src/endpoints/levels.cc ================================================ #include "motis/endpoints/levels.h" #include "net/bad_request_exception.h" #include "utl/pipes/all.h" #include "utl/pipes/vec.h" #include "utl/to_vec.h" #include "osr/lookup.h" #include "motis/parse_location.h" #include "motis/types.h" namespace json = boost::json; namespace motis::ep { api::levels_response levels::operator()( boost::urls::url_view const& url) const { auto const query = api::levels_params{url.params()}; auto const min = parse_location(query.min_); auto const max = parse_location(query.max_); utl::verify( min.has_value(), "min not a coordinate: {}", query.min_); utl::verify( max.has_value(), "max not a coordinate: {}", query.max_); auto levels = hash_set{}; l_.find({min->pos_, max->pos_}, [&](osr::way_idx_t const x) { auto const p = w_.r_->way_properties_[x]; levels.emplace(p.from_level().to_float()); levels.emplace(p.to_level().to_float()); }); auto levels_sorted = utl::to_vec(levels, [](float const l) { return static_cast(l); }); utl::sort(levels_sorted, [](auto&& a, auto&& b) { return a > b; }); return levels_sorted; } } // namespace motis::ep ================================================ FILE: src/endpoints/map/flex.cc ================================================ #include "motis/endpoints/map/flex_locations.h" #include "utl/to_vec.h" #include "net/bad_request_exception.h" #include "osr/geojson.h" #include "nigiri/timetable.h" #include "motis-api/motis-api.h" #include "motis/parse_location.h" #include "motis/tag_lookup.h" namespace json = boost::json; namespace n = nigiri; namespace motis::ep { json::value to_geometry(n::timetable const& tt, n::flex_area_idx_t const a) { auto const ring_to_json = [](auto&& r) { return utl::transform_to( r, [](geo::latlng const& x) { return osr::to_array(x); }); }; auto const get_rings = [&](unsigned const i) { auto rings = json::array{}; rings.emplace_back(ring_to_json(tt.flex_area_outers_[a][i])); for (auto const r : tt.flex_area_inners_[a][i]) { rings.emplace_back(ring_to_json(r)); } return rings; }; if (tt.flex_area_outers_[a].size() == 1U) { return {{"type", "Polygon"}, {"coordinates", get_rings(0U)}}; } else { auto rings = json::array{}; for (auto i = 0U; i != tt.flex_area_outers_[a].size(); ++i) { rings.emplace_back(get_rings(i)); } return {{"type", "MultiPolygon"}, {"coordinates", rings}}; } } boost::json::value flex_locations::operator()( boost::urls::url_view const& url) const { auto const query = api::stops_params{url.params()}; auto const min = parse_location(query.min_); auto const max = parse_location(query.max_); utl::verify( min.has_value(), "min not a coordinate: {}", query.min_); utl::verify( max.has_value(), "max not a coordinate: {}", query.max_); auto features = json::array{}; tt_.flex_area_rtree_.search( min->pos_.lnglat_float(), max->pos_.lnglat_float(), [&](auto&&, auto&&, n::flex_area_idx_t const a) { features.emplace_back(json::value{ {"type", "Feature"}, {"id", tt_.strings_.get(tt_.flex_area_id_[a])}, {"geometry", to_geometry(tt_, a)}, {"properties", {{"stop_name", tt_.translate(query.language_, tt_.flex_area_name_[a])}, {"stop_desc", tt_.translate(query.language_, tt_.flex_area_desc_[a])}}}}); return true; }); loc_rtree_.find({min->pos_, max->pos_}, [&](n::location_idx_t const l) { if (!tt_.location_location_groups_[l].empty()) { features.emplace_back(json::value{ {"type", "Feature"}, {"id", tags_.id(tt_, l)}, {"geometry", osr::to_point(osr::point::from_latlng( tt_.locations_.coordinates_[l]))}, {"properties", {{"name", tt_.translate(query.language_, tt_.locations_.names_[l])}, {"location_groups", utl::transform_to( tt_.location_location_groups_[l], [&](n::location_group_idx_t const l) -> json::string { return {tt_.translate(query.language_, tt_.location_group_name_[l])}; })}}}}); } return true; }); return {{"type", "FeatureCollection"}, {"features", std::move(features)}}; } } // namespace motis::ep ================================================ FILE: src/endpoints/map/rental.cc ================================================ #include "motis/endpoints/map/rental.h" #include #include #include #include #include #include #include "utl/enumerate.h" #include "utl/helpers/algorithm.h" #include "utl/overloaded.h" #include "utl/to_vec.h" #include "geo/box.h" #include "geo/polyline.h" #include "geo/polyline_format.h" #include "nigiri/timetable.h" #include "motis-api/motis-api.h" #include "motis/gbfs/data.h" #include "motis/gbfs/mode.h" #include "motis/parse_location.h" #include "motis/place.h" namespace json = boost::json; namespace motis::ep { api::rentals_response rental::operator()( boost::urls::url_view const& url) const { auto const parse_loc = [](std::string_view const sv) { return parse_location(sv); }; auto const parse_place_pos = [&](std::string_view const sv) { auto const place = get_place(tt_, tags_, sv); return std::visit( utl::overloaded{[&](osr::location const& l) { return l.pos_; }, [&](tt_location const tt_l) { return tt_->locations_.coordinates_.at(tt_l.l_); }}, place); }; auto const query = api::rentals_params{url.params()}; auto const min = query.min_.and_then(parse_loc); auto const max = query.max_.and_then(parse_loc); auto const point_pos = query.point_.transform(parse_place_pos); auto const point_radius = query.radius_; auto const filter_bbox = min.has_value() && max.has_value(); auto const filter_point = point_pos.has_value() && point_radius.has_value(); auto const filter_providers = query.providers_.has_value() && !query.providers_->empty(); auto const filter_groups = query.providerGroups_.has_value() && !query.providerGroups_->empty(); auto gbfs = gbfs_; auto res = api::rentals_response{}; if (gbfs == nullptr) { return res; } auto const restrictions_to_api = [&](gbfs::geofencing_restrictions const& r) { return api::RentalZoneRestrictions{ .vehicleTypeIdxs_ = {}, .rideStartAllowed_ = r.ride_start_allowed_, .rideEndAllowed_ = r.ride_end_allowed_, .rideThroughAllowed_ = r.ride_through_allowed_, .stationParking_ = r.station_parking_}; }; auto const rule_to_api = [&](gbfs::rule const& r) { return api::RentalZoneRestrictions{ .vehicleTypeIdxs_ = utl::to_vec(r.vehicle_type_idxs_, [&](auto const vti) { return static_cast(to_idx(vti)); }), .rideStartAllowed_ = r.ride_start_allowed_, .rideEndAllowed_ = r.ride_end_allowed_, .rideThroughAllowed_ = r.ride_through_allowed_, .stationParking_ = r.station_parking_}; }; auto const ring_to_api = [&](tg_ring const* ring) { auto enc = geo::polyline_encoder<6>{}; auto const np = tg_ring_num_points(ring); for (auto i = 0; i != np; ++i) { auto const pt = tg_ring_point_at(ring, i); enc.push(geo::latlng{pt.y, pt.x}); } return api::EncodedPolyline{ .points_ = std::move(enc.buf_), .precision_ = 6, .length_ = np}; }; auto const multipoly_to_api = [&](tg_geom* const geom) { assert(tg_geom_typeof(geom) == TG_MULTIPOLYGON); auto mp = api::MultiPolygon{}; for (auto i = 0; i != tg_geom_num_polys(geom); ++i) { auto const* poly = tg_geom_poly_at(geom, i); auto polylines = std::vector{}; polylines.emplace_back(ring_to_api(tg_poly_exterior(poly))); for (int j = 0; j != tg_poly_num_holes(poly); ++j) { polylines.emplace_back(ring_to_api(tg_poly_hole_at(poly, j))); } mp.push_back(std::move(polylines)); } return mp; }; auto const add_provider = [&](gbfs::gbfs_provider const* provider) { if (!query.withProviders_) { return; } auto form_factors = std::vector{}; for (auto const& vt : provider->vehicle_types_) { auto const ff = gbfs::to_api_form_factor(vt.form_factor_); if (utl::find(form_factors, ff) == end(form_factors)) { form_factors.push_back(ff); } } res.providers_.emplace_back(api::RentalProvider{ .id_ = provider->id_, .name_ = provider->sys_info_.name_, .groupId_ = provider->group_id_, .operator_ = provider->sys_info_.operator_, .url_ = provider->sys_info_.url_, .purchaseUrl_ = provider->sys_info_.purchase_url_, .color_ = provider->color_, .bbox_ = {provider->bbox_.min_.lng_, provider->bbox_.min_.lat_, provider->bbox_.max_.lng_, provider->bbox_.max_.lat_}, .vehicleTypes_ = utl::to_vec( provider->vehicle_types_, [&](gbfs::vehicle_type const& vt) { return api::RentalVehicleType{ .id_ = vt.id_, .name_ = vt.name_, .formFactor_ = gbfs::to_api_form_factor(vt.form_factor_), .propulsionType_ = gbfs::to_api_propulsion_type(vt.propulsion_type_), .returnConstraint_ = gbfs::to_api_return_constraint(vt.return_constraint_), .returnConstraintGuessed_ = !vt.known_return_constraint_}; }), .formFactors_ = std::move(form_factors), .defaultRestrictions_ = restrictions_to_api(provider->default_restrictions_), .globalGeofencingRules_ = utl::to_vec(provider->geofencing_zones_.global_rules_, [&](gbfs::rule const& r) { return rule_to_api(r); })}); }; auto const add_provider_group = [&](gbfs::gbfs_group const& group) { auto form_factors = std::vector{}; auto color = group.color_; for (auto const& pi : group.providers_) { auto const& provider = gbfs->providers_.at(pi); if (provider == nullptr) { // shouldn't be possible, but just in case... std::cerr << "[rental api] warning: provider group " << group.id_ << " references missing provider idx " << to_idx(pi) << "\n"; continue; } for (auto const& vt : provider->vehicle_types_) { auto const ff = gbfs::to_api_form_factor(vt.form_factor_); if (utl::find(form_factors, ff) == end(form_factors)) { form_factors.push_back(ff); } if (!color && provider->color_) { color = provider->color_; } } } auto provider_ids = std::vector{}; if (query.withProviders_) { provider_ids.reserve(group.providers_.size()); for (auto const& pi : group.providers_) { auto const& provider = gbfs->providers_.at(pi); if (provider == nullptr) { // shouldn't be possible, but just in case... std::cerr << "[rental api] warning: provider group " << group.id_ << " references missing provider idx " << to_idx(pi) << " (providers list)\n"; continue; } provider_ids.push_back(provider->id_); } } res.providerGroups_.emplace_back( api::RentalProviderGroup{.id_ = group.id_, .name_ = group.name_, .color_ = color, .providers_ = std::move(provider_ids), .formFactors_ = form_factors}); }; if (!filter_bbox && !filter_point && !filter_providers && !filter_groups) { for (auto const& provider : gbfs->providers_) { if (provider != nullptr) { add_provider(provider.get()); } } for (auto const& group : gbfs->groups_ | std::views::values) { add_provider_group(group); } return res; } auto bbox = filter_bbox ? geo::box{min->pos_, max->pos_} : geo::box{}; auto const in_bbox = [&](geo::latlng const& pos) { return filter_bbox ? bbox.contains(pos) : true; }; auto const approx_distance_lng_degrees = point_pos ? geo::approx_distance_lng_degrees(*point_pos) : 0.0; auto const point_radius_squared = point_radius ? (*point_radius) * (*point_radius) : 0.0; auto const in_radius = [&](geo::latlng const& pos) { return filter_point ? geo::approx_squared_distance( *point_pos, pos, approx_distance_lng_degrees) <= point_radius_squared : true; }; auto const include_bbox = [&](geo::box const& b) { if (filter_bbox && !b.overlaps(bbox)) { return false; } if (filter_point) { auto const closest = geo::latlng{std::clamp(point_pos->lat(), b.min_.lat(), b.max_.lat()), std::clamp(point_pos->lng(), b.min_.lng(), b.max_.lng())}; return geo::approx_squared_distance(*point_pos, closest, approx_distance_lng_degrees) <= point_radius_squared; } return true; }; auto providers = hash_set{}; auto provider_groups = hash_set{}; auto check_provider = [&](gbfs_provider_idx_t const pi) { auto const& provider = gbfs->providers_.at(pi); if (provider == nullptr || providers.contains(provider.get())) { return; } if ((filter_providers || filter_groups) && !((filter_providers && utl::find(*query.providers_, provider->id_) != end(*query.providers_)) || (filter_groups && utl::find(*query.providerGroups_, provider->group_id_) != end(*query.providerGroups_)))) { return; } providers.insert(provider.get()); provider_groups.insert(provider->group_id_); }; if (filter_point) { gbfs->provider_rtree_.in_radius( *point_pos, *point_radius, [&](gbfs_provider_idx_t const pi) { check_provider(pi); }); gbfs->provider_zone_rtree_.in_radius( *point_pos, *point_radius, [&](gbfs_provider_idx_t const pi) { check_provider(pi); }); } else if (filter_bbox) { gbfs->provider_rtree_.find( bbox, [&](gbfs_provider_idx_t const pi) { check_provider(pi); }); gbfs->provider_zone_rtree_.find( bbox, [&](gbfs_provider_idx_t const pi) { check_provider(pi); }); } else if (filter_providers || filter_groups) { if (filter_providers) { for (auto const& id : *query.providers_) { if (auto const it = gbfs->provider_by_id_.find(id); it != end(gbfs->provider_by_id_)) { auto const& provider = gbfs->providers_.at(it->second); if (provider != nullptr) { providers.insert(provider.get()); provider_groups.insert(provider->group_id_); } } } } if (filter_groups) { for (auto const& id : *query.providerGroups_) { if (auto const it = gbfs->groups_.find(id); it != end(gbfs->groups_)) { provider_groups.insert(it->second.id_); for (auto const pi : it->second.providers_) { auto const& provider = gbfs->providers_.at(pi); if (provider == nullptr) { // shouldn't be possible, but just in case... std::cerr << "[rental api] warning: provider group " << it->second.id_ << " references missing provider idx " << to_idx(pi) << "\n"; continue; } providers.insert(provider.get()); } } } } } for (auto const* provider : providers) { add_provider(provider); if (query.withStations_) { for (auto const& st : provider->stations_ | std::views::values) { auto const sbb = st.info_.bounding_box(); if (!include_bbox(sbb)) { continue; } auto form_factor_counts = std::array{}; auto types_available = std::map{}; auto docks_available = std::map{}; auto form_factors = std::set{}; for (auto const& [vti, count] : st.status_.vehicle_types_available_) { if (vti == gbfs::vehicle_type_idx_t::invalid() || cista::to_idx(vti) >= provider->vehicle_types_.size()) { continue; } auto const& vt = provider->vehicle_types_.at(vti); auto const api_ff = gbfs::to_api_form_factor(vt.form_factor_); form_factor_counts[static_cast( std::to_underlying(api_ff))] += count; types_available[vt.id_] = count; form_factors.insert(api_ff); } for (auto const& [vti, count] : st.status_.vehicle_docks_available_) { if (vti == gbfs::vehicle_type_idx_t::invalid() || cista::to_idx(vti) >= provider->vehicle_types_.size()) { continue; } auto const& vt = provider->vehicle_types_.at(vti); auto const api_ff = gbfs::to_api_form_factor(vt.form_factor_); form_factor_counts[static_cast( std::to_underlying(api_ff))] += count; docks_available[vt.id_] = count; form_factors.insert(api_ff); } if (form_factors.empty()) { for (auto const& vt : provider->vehicle_types_) { form_factors.insert(gbfs::to_api_form_factor(vt.form_factor_)); } } auto sorted_form_factors = utl::to_vec(form_factors); utl::sort(sorted_form_factors, [&](auto const a, auto const b) { return form_factor_counts[static_cast( std::to_underlying(a))] > form_factor_counts[static_cast( std::to_underlying(b))]; }); res.stations_.emplace_back(api::RentalStation{ .id_ = st.info_.id_, .providerId_ = provider->id_, .providerGroupId_ = provider->group_id_, .name_ = st.info_.name_, .lat_ = st.info_.pos_.lat_, .lon_ = st.info_.pos_.lng_, .address_ = st.info_.address_, .crossStreet_ = st.info_.cross_street_, .rentalUriAndroid_ = st.info_.rental_uris_.android_, .rentalUriIOS_ = st.info_.rental_uris_.ios_, .rentalUriWeb_ = st.info_.rental_uris_.web_, .isRenting_ = st.status_.is_renting_, .isReturning_ = st.status_.is_returning_, .numVehiclesAvailable_ = st.status_.num_vehicles_available_, .formFactors_ = sorted_form_factors, .vehicleTypesAvailable_ = std::move(types_available), .vehicleDocksAvailable_ = std::move(docks_available), .stationArea_ = st.info_.station_area_ != nullptr ? std::optional{multipoly_to_api( st.info_.station_area_.get())} : std::nullopt, .bbox_ = {sbb.min_.lng_, sbb.min_.lat_, sbb.max_.lng_, sbb.max_.lat_}}); } } if (query.withVehicles_) { auto const add_vehicle = [&](gbfs::vehicle_status const& vs, gbfs::vehicle_type const& vt) { res.vehicles_.emplace_back(api::RentalVehicle{ .id_ = vs.id_, .providerId_ = provider->id_, .providerGroupId_ = provider->group_id_, .typeId_ = vt.id_, .lat_ = vs.pos_.lat_, .lon_ = vs.pos_.lng_, .formFactor_ = gbfs::to_api_form_factor(vt.form_factor_), .propulsionType_ = gbfs::to_api_propulsion_type(vt.propulsion_type_), .returnConstraint_ = gbfs::to_api_return_constraint(vt.return_constraint_), .stationId_ = vs.station_id_, .homeStationId_ = vs.home_station_id_, .isReserved_ = vs.is_reserved_, .isDisabled_ = vs.is_disabled_, .rentalUriAndroid_ = vs.rental_uris_.android_, .rentalUriIOS_ = vs.rental_uris_.ios_, .rentalUriWeb_ = vs.rental_uris_.web_, }); }; auto const fallback_vt = gbfs::vehicle_type{.form_factor_ = gbfs::vehicle_form_factor::kOther}; for (auto const& vs : provider->vehicle_status_) { if (in_bbox(vs.pos_) && in_radius(vs.pos_)) { if (vs.vehicle_type_idx_ != gbfs::vehicle_type_idx_t::invalid() && cista::to_idx(vs.vehicle_type_idx_) < provider->vehicle_types_.size()) { auto const& vt = provider->vehicle_types_.at(vs.vehicle_type_idx_); add_vehicle(vs, vt); } else { add_vehicle(vs, fallback_vt); } } } } if (query.withZones_) { auto const n_zones = static_cast(provider->geofencing_zones_.zones_.size()); for (auto const [order, zone] : utl::enumerate(provider->geofencing_zones_.zones_)) { auto const zbb = zone.bounding_box(); if (!include_bbox(zbb)) { continue; } res.zones_.emplace_back(api::RentalZone{ .providerId_ = provider->id_, .providerGroupId_ = provider->group_id_, .name_ = zone.name_, .z_ = n_zones - static_cast(order), .bbox_ = {zbb.min_.lng_, zbb.min_.lat_, zbb.max_.lng_, zbb.max_.lat_}, .area_ = multipoly_to_api(zone.geom_.get()), .rules_ = utl::to_vec( zone.rules_, [&](gbfs::rule const& r) { return rule_to_api(r); }), }); } } } for (auto const& group_id : provider_groups) { add_provider_group(gbfs->groups_.at(group_id)); } return res; } } // namespace motis::ep ================================================ FILE: src/endpoints/map/route_details.cc ================================================ #include "motis/endpoints/map/route_details.h" #include "motis-api/motis-api.h" #include "motis/data.h" #include "motis/fwd.h" #include "motis/railviz.h" #include "motis/server.h" namespace motis::ep { api::routeDetails_response route_details::operator()( boost::urls::url_view const& url) const { auto const api_version = get_api_version(url); auto const rt = rt_; return get_route_details(tags_, tt_, rt->rtt_.get(), shapes_, w_, pl_, matches_, ae_, tz_, *static_.impl_, *rt->railviz_rt_->impl_, api::routeDetails_params{url.params()}, api_version); } } // namespace motis::ep ================================================ FILE: src/endpoints/map/routes.cc ================================================ #include "motis/endpoints/map/routes.h" #include "motis-api/motis-api.h" #include "motis/data.h" #include "motis/fwd.h" #include "motis/railviz.h" #include "motis/server.h" namespace motis::ep { api::routes_response routes::operator()( boost::urls::url_view const& url) const { auto const api_version = get_api_version(url); auto const rt = rt_; return get_routes(tags_, tt_, rt->rtt_.get(), shapes_, w_, pl_, matches_, ae_, tz_, *static_.impl_, *rt->railviz_rt_->impl_, api::routes_params{url.params()}, api_version); } } // namespace motis::ep ================================================ FILE: src/endpoints/map/shapes_debug.cc ================================================ #include "motis/endpoints/map/shapes_debug.h" #include #include #include #include #include #include "boost/json.hpp" #include "fmt/format.h" #include "utl/to_vec.h" #include "utl/verify.h" #include "nigiri/rt/frun.h" #include "nigiri/timetable.h" #include "nigiri/types.h" #include "osr/routing/map_matching_debug.h" #include "motis/route_shapes.h" #include "motis/tag_lookup.h" namespace motis::ep { namespace { std::uint64_t parse_route_idx(std::string_view const path) { auto const slash = path.find_last_of('/'); auto const idx_str = slash == std::string_view::npos ? path : path.substr(slash + 1U); utl::verify(!idx_str.empty(), "missing route index"); auto route_idx = std::uint64_t{}; auto const* begin = idx_str.data(); auto const* end = begin + idx_str.size(); auto const [ptr, ec] = std::from_chars(begin, end, route_idx); utl::verify(ec == std::errc{} && ptr == end, "invalid route index '{}'", idx_str); return route_idx; } boost::json::object build_caller_data(nigiri::timetable const& tt, tag_lookup const& tags, nigiri::route_idx_t const route, std::uint64_t const route_idx, nigiri::clasz const clasz) { auto const lang = nigiri::lang_t{}; auto const stop_count = static_cast(tt.route_location_seq_[route].size()); auto route_infos = std::set>{}; auto trip_ids = boost::json::array{}; for (auto const transport_idx : tt.route_transport_ranges_[route]) { auto const fr = nigiri::rt::frun{ tt, nullptr, nigiri::rt::run{ .t_ = nigiri::transport{transport_idx, nigiri::day_idx_t{0}}, .stop_range_ = nigiri::interval{nigiri::stop_idx_t{0U}, stop_count}, .rt_ = nigiri::rt_transport_idx_t::invalid()}}; auto const first = fr[nigiri::stop_idx_t{0U}]; route_infos.emplace( std::string{first.get_route_id(nigiri::event_type::kDep)}, std::string{first.route_short_name(nigiri::event_type::kDep, lang)}, std::string{first.route_long_name(nigiri::event_type::kDep, lang)}); trip_ids.emplace_back( boost::json::string{tags.id(tt, first, nigiri::event_type::kDep)}); } return boost::json::object{ {"route_index", route_idx}, {"route_clasz", to_str(clasz)}, {"timetable_routes", utl::transform_to( route_infos, [](auto const& info) { auto const& [id, short_name, long_name] = info; return boost::json::object{ {"id", id}, {"short_name", short_name}, {"long_name", long_name}}; })}, {"trip_ids", std::move(trip_ids)}}; } } // namespace net::reply shapes_debug::operator()(net::route_request const& req, bool) const { utl::verify(c_.shapes_debug_api_enabled(), "route shapes debug API is disabled"); utl::verify( w_ != nullptr && l_ != nullptr && tt_ != nullptr && tags_ != nullptr, "data not loaded"); auto const url = boost::url_view{req.target()}; auto const route_idx = parse_route_idx(url.path()); utl::verify(route_idx < tt_->n_routes(), "invalid route index {} (max={})", route_idx, tt_->n_routes() == 0U ? 0U : tt_->n_routes() - 1U); auto const route = nigiri::route_idx_t{static_cast(route_idx)}; auto const clasz = tt_->route_clasz_[route]; auto debug_json = route_shape_debug(*w_, *l_, *tt_, route); debug_json["caller"] = build_caller_data(*tt_, *tags_, route, route_idx, clasz); auto payload = osr::gzip_json(debug_json); auto const filename = fmt::format("r_{}_{}.json.gz", route_idx, to_str(clasz)); auto res = net::web_server::string_res_t{boost::beast::http::status::ok, req.version()}; res.insert(boost::beast::http::field::content_type, "application/gzip"); res.insert(boost::beast::http::field::content_disposition, fmt::format("attachment; filename=\"{}\"", filename)); res.insert(boost::beast::http::field::access_control_expose_headers, "content-disposition"); res.body() = std::move(payload); res.keep_alive(req.keep_alive()); return res; } } // namespace motis::ep ================================================ FILE: src/endpoints/map/stops.cc ================================================ #include "motis/endpoints/map/stops.h" #include "net/bad_request_exception.h" #include "net/too_many_exception.h" #include "osr/geojson.h" #include "motis/journey_to_response.h" #include "motis/parse_location.h" #include "motis/tag_lookup.h" namespace json = boost::json; namespace n = nigiri; namespace motis::ep { api::stops_response stops::operator()(boost::urls::url_view const& url) const { auto const query = api::stops_params{url.params()}; auto const min = parse_location(query.min_); auto const max = parse_location(query.max_); utl::verify( min.has_value(), "min not a coordinate: {}", query.min_); utl::verify( max.has_value(), "max not a coordinate: {}request_exception", query.max_); auto res = api::stops_response{}; auto const max_results = config_.get_limits().stops_max_results_; loc_rtree_.find({min->pos_, max->pos_}, [&](n::location_idx_t const l) { utl::verify(res.size() < max_results, "too many items"); res.emplace_back(to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, query.language_, tt_location{l})); }); return res; } } // namespace motis::ep ================================================ FILE: src/endpoints/map/trips.cc ================================================ #include "motis/endpoints/map/trips.h" #include "motis-api/motis-api.h" #include "motis/data.h" #include "motis/fwd.h" #include "motis/railviz.h" #include "motis/server.h" namespace motis::ep { api::trips_response trips::operator()(boost::urls::url_view const& url) const { auto const api_version = get_api_version(url); auto const rt = std::atomic_load(&rt_); return get_trains(tags_, tt_, rt->rtt_.get(), shapes_, w_, pl_, matches_, ae_, tz_, *static_.impl_, *rt->railviz_rt_->impl_, api::trips_params{url.params()}, api_version); } } // namespace motis::ep ================================================ FILE: src/endpoints/matches.cc ================================================ #include "motis/endpoints/matches.h" #include "net/too_many_exception.h" #include "osr/geojson.h" #include "motis/location_routes.h" #include "motis/match_platforms.h" #include "motis/tag_lookup.h" namespace json = boost::json; namespace n = nigiri; namespace motis::ep { constexpr auto const kLimit = 2048; std::string get_names(osr::platforms const& pl, osr::platform_idx_t const x) { auto ss = std::stringstream{}; for (auto const y : pl.platform_names_[x]) { ss << y.view() << ", "; } return ss.str(); } json::value matches::operator()(json::value const& query) const { auto const& q = query.as_array(); auto const min = geo::latlng{q[1].as_double(), q[0].as_double()}; auto const max = geo::latlng{q[3].as_double(), q[2].as_double()}; auto matches = json::array{}; pl_.find(min, max, [&](osr::platform_idx_t const p) { utl::verify(matches.size() < kLimit, "too many items"); auto const center = get_platform_center(pl_, w_, p); if (!center.has_value()) { return; } auto platform_data = json::value{{"type", "platform"}, {"level", pl_.get_level(w_, p).to_float()}, {"platform_names", fmt::format("{}", get_names(pl_, p))}} .as_object(); std::visit(utl::overloaded{[&](osr::way_idx_t x) { platform_data.emplace( "osm_way_id", to_idx(w_.way_osm_idx_[x])); }, [&](osr::node_idx_t x) { platform_data.emplace( "osm_node_id", to_idx(w_.node_to_osm_[x])); }}, osr::to_ref(pl_.platform_ref_[p][0])); matches.emplace_back(json::value{ {"type", "Feature"}, {"properties", platform_data}, {"geometry", osr::to_point(osr::point::from_latlng(*center))}}); }); loc_rtree_.find({min, max}, [&](n::location_idx_t const l) { utl::verify(matches.size() < kLimit, "too many items"); auto const pos = tt_.locations_.coordinates_[l]; auto const match = get_match(tt_, pl_, w_, l); auto props = json::value{ {"name", tt_.get_default_translation(tt_.locations_.names_[l])}, {"id", tags_.id(tt_, l)}, {"src", to_idx(tt_.locations_.src_[l])}, {"platform_codes", tt_.get_default_translation(tt_.locations_.platform_codes_[l])}, {"type", "location"}, {"trips", fmt::format("{}", get_location_routes(tt_, l))}} .as_object(); if (match == osr::platform_idx_t::invalid()) { props.emplace("level", "-"); } else { std::visit(utl::overloaded{ [&](osr::way_idx_t x) { props.emplace("osm_way_id", to_idx(w_.way_osm_idx_[x])); props.emplace( "level", w_.r_->way_properties_[x].from_level().to_float()); }, [&](osr::node_idx_t x) { props.emplace("osm_node_id", to_idx(w_.node_to_osm_[x])); props.emplace( "level", w_.r_->node_properties_[x].from_level().to_float()); }}, osr::to_ref(pl_.platform_ref_[match][0])); } matches.emplace_back( json::value{{"type", "Feature"}, {"properties", props}, {"geometry", osr::to_point(osr::point::from_latlng(pos))}}); if (match == osr::platform_idx_t::invalid()) { return; } props.emplace("platform_names", fmt::format("{}", get_names(pl_, match))); auto const center = get_platform_center(pl_, w_, match); if (!center.has_value()) { return; } props.insert_or_assign("type", "match"); matches.emplace_back(json::value{ {"type", "Feature"}, {"properties", props}, {"geometry", osr::to_line_string({osr::point::from_latlng(*center), osr::point::from_latlng(pos)})}}); }); return json::value{{"type", "FeatureCollection"}, {"features", matches}}; } } // namespace motis::ep ================================================ FILE: src/endpoints/metrics.cc ================================================ #include "motis/endpoints/metrics.h" #include #include #include #include "prometheus/registry.h" #include "prometheus/text_serializer.h" #include "utl/enumerate.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/timetable.h" #include "nigiri/timetable_metrics.h" #include "nigiri/types.h" #include "motis/data.h" #include "motis/tag_lookup.h" namespace n = nigiri; namespace motis::ep { void update_all_runs_metrics(nigiri::timetable const& tt, nigiri::rt_timetable const* rtt, tag_lookup const& tags, metrics_registry& metrics) { auto const start_time = std::max(std::chrono::time_point_cast( std::chrono::system_clock::now()), tt.external_interval().from_); auto const end_time = std::min( std::chrono::time_point_cast( start_time + std::chrono::duration_cast(std::chrono::minutes{3})), tt.external_interval().to_); auto const time_interval = n::interval{start_time, end_time}; auto metric_by_agency = std::vector, std::reference_wrapper>>{}; metric_by_agency.reserve(tt.n_agencies()); for (auto i = nigiri::provider_idx_t{0}; i < tt.n_agencies(); ++i) { auto const& p = tt.providers_[i]; auto const labels = prometheus::Labels{ {"tag", std::string{tags.get_tag(p.src_)}}, {"agency_name", std::string{tt.get_default_translation(p.name_)}}, {"agency_id", std::string{tt.strings_.get(p.id_)}}}; auto& sched = metrics.current_trips_running_scheduled_count_.Add(labels); auto& real = metrics.current_trips_running_scheduled_with_realtime_count_.Add( labels); sched.Set(0); real.Set(0); metric_by_agency.emplace_back(std::ref(sched), std::ref(real)); } if (rtt != nullptr) { for (auto rt_t = nigiri::rt_transport_idx_t{0}; rt_t < rtt->n_rt_transports(); ++rt_t) { auto const fr = n::rt::frun::from_rt(tt, rtt, rt_t); if (!fr.is_scheduled()) { continue; } auto const active = n::interval{ fr[0].time(n::event_type::kDep), fr[static_cast(fr.stop_range_.size() - 1)].time( n::event_type::kArr) + n::unixtime_t::duration{1}}; if (active.overlaps(time_interval)) { auto const provider_idx = fr[0].get_provider_idx(n::event_type::kDep); if (provider_idx != n::provider_idx_t::invalid()) { metric_by_agency.at(provider_idx.v_).first.get().Increment(); metric_by_agency.at(provider_idx.v_).second.get().Increment(); } } } } auto const [start_day, _] = tt.day_idx_mam(time_interval.from_); auto const [end_day, _1] = tt.day_idx_mam(time_interval.to_); for (auto r = nigiri::route_idx_t{0}; r < tt.n_routes(); ++r) { auto const is_active = [&](n::transport const t) -> bool { return (rtt == nullptr ? tt.bitfields_[tt.transport_traffic_days_[t.t_idx_]] : rtt->bitfields_[rtt->transport_traffic_days_[t.t_idx_]]) .test(to_idx(t.day_)); }; auto const seq = tt.route_location_seq_[r]; auto const from = n::stop_idx_t{0U}; auto const to = static_cast(seq.size() - 1); auto const arr_times = tt.event_times_at_stop(r, to, n::event_type::kArr); for (auto const [i, t_idx] : utl::enumerate(tt.route_transport_ranges_[r])) { auto const day_offset = static_cast(arr_times[i].days()); for (auto day = start_day - day_offset; day <= end_day; ++day) { auto const t = n::transport{t_idx, day}; if (is_active(t) && time_interval.overlaps({tt.event_time(t, from, n::event_type::kDep), tt.event_time(t, to, n::event_type::kArr) + n::unixtime_t::duration{1}})) { auto fr = n::rt::frun::from_t(tt, nullptr, t); auto const provider_idx = fr[0].get_provider_idx(n::event_type::kDep); if (provider_idx != n::provider_idx_t::invalid()) { metric_by_agency.at(provider_idx.v_).first.get().Increment(); } } } } } if (metrics.timetable_first_day_timestamp_.Collect().empty()) { auto const m = get_metrics(tt); constexpr auto const kEndOfDay = std::chrono::days{1} - std::chrono::seconds{1}; auto const from = std::chrono::duration_cast( tt.internal_interval().from_.time_since_epoch()); for (auto src = n::source_idx_t{0U}; src != tt.n_sources(); ++src) { auto const& fm = m.feeds_[src]; auto const labels = prometheus::Labels{{"tag", std::string{tags.get_tag(src)}}}; metrics.timetable_first_day_timestamp_.Add( labels, static_cast((from + date::days{fm.first_}).count())); metrics.timetable_last_day_timestamp_.Add( labels, static_cast( (from + date::days{fm.last_} + kEndOfDay).count())); metrics.timetable_locations_count_.Add(labels, fm.locations_); metrics.timetable_trips_count_.Add(labels, fm.trips_); metrics.timetable_transports_x_days_count_.Add( labels, static_cast(fm.transport_days_)); } } } net::reply metrics::operator()(net::route_request const& req, bool) const { utl::verify(metrics_ != nullptr && tt_ != nullptr && tags_ != nullptr, "no metrics initialized"); auto const rt = std::atomic_load(&rt_); update_all_runs_metrics(*tt_, rt->rtt_.get(), *tags_, *metrics_); metrics_->total_trips_with_realtime_count_.Set( static_cast(rt->rtt_->rt_transport_src_.size())); auto res = net::web_server::string_res_t{boost::beast::http::status::ok, req.version()}; res.insert(boost::beast::http::field::content_type, "text/plain; version=0.0.4"); set_response_body( res, req, prometheus::TextSerializer{}.Serialize(metrics_->registry_.Collect())); res.keep_alive(req.keep_alive()); return res; } } // namespace motis::ep ================================================ FILE: src/endpoints/ojp.cc ================================================ #include "motis/endpoints/ojp.h" #include "pugixml.hpp" #include #include "fmt/format.h" #include "date/date.h" #include "geo/polyline_format.h" #include "net/bad_request_exception.h" #include "nigiri/timetable.h" #include "motis/adr_extend_tt.h" #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" namespace n = nigiri; namespace sr = std::ranges; using namespace std::string_view_literals; namespace motis::ep { template T* maybe_ref(T& x) { return &x; } template T* maybe_ref(T* x) { return x; } template T& maybe_deref(T& x) { return x; } template T& maybe_deref(T* x) { utl::verify(x != nullptr, "not set: {}", cista::type_str()); return *x; } static auto response_id = std::atomic_size_t{0U}; struct transport_mode { std::string_view transport_mode_; std::string_view transport_submode_type_; std::string_view transport_submode_name_; }; transport_mode get_transport_mode(std::int64_t const route_type) { switch (route_type) { case 0: return {"tram", "TramSubmode", ""}; case 1: return {"metro", "MetroSubmode", ""}; case 2: return {"rail", "RailSubmode", ""}; case 3: return {"bus", "BusSubmode", ""}; case 4: return {"ferry", "", ""}; case 5: return {"cableway", "TelecabinSubmode", "cableCar"}; case 6: return {"telecabin", "TelecabinSubmode", "telecabin"}; case 7: return {"funicular", "", ""}; case 11: return {"trolleyBus", "", ""}; case 12: return {"rail", "RailSubmode", "monorail"}; case 100: return {"rail", "RailSubmode", ""}; case 101: return {"rail", "RailSubmode", "highSpeedRail"}; case 102: return {"rail", "RailSubmode", "longDistance"}; case 103: return {"rail", "RailSubmode", "interregionalRail"}; case 104: return {"rail", "RailSubmode", "carTransportRailService"}; case 105: return {"rail", "RailSubmode", "nightRail"}; case 106: return {"rail", "RailSubmode", "regionalRail"}; case 107: return {"rail", "RailSubmode", "touristRailway"}; case 108: return {"rail", "RailSubmode", "railShuttle"}; case 109: return {"rail", "RailSubmode", "suburbanRailway"}; case 110: return {"rail", "RailSubmode", "replacementRailService"}; case 111: return {"rail", "RailSubmode", "specialTrain"}; case 200: return {"coach", "CoachSubmode", ""}; case 201: return {"coach", "CoachSubmode", "internationalCoach"}; case 202: return {"coach", "CoachSubmode", "nationalCoach"}; case 203: return {"coach", "CoachSubmode", "shuttleCoach"}; case 204: return {"coach", "CoachSubmode", "regionalCoach"}; case 205: return {"coach", "CoachSubmode", "specialCoach"}; case 206: return {"coach", "CoachSubmode", "sightseeingCoach"}; case 207: return {"coach", "CoachSubmode", "touristCoach"}; case 208: return {"coach", "CoachSubmode", "commuterCoach"}; case 400: return {"metro", "MetroSubmode", "urbanRailway"}; case 401: return {"metro", "MetroSubmode", "metro"}; case 402: return {"metro", "MetroSubmode", "tube"}; case 700: return {"bus", "BusSubmode", ""}; case 701: return {"bus", "BusSubmode", "regionalBus"}; case 702: return {"bus", "BusSubmode", "expressBus"}; case 704: return {"bus", "BusSubmode", "localBus"}; case 709: return {"bus", "BusSubmode", "mobilityBusForRegisteredDisabled"}; case 710: return {"bus", "BusSubmode", "sightseeingBus"}; case 711: return {"bus", "BusSubmode", "shuttleBus"}; case 712: return {"bus", "BusSubmode", "schoolBus"}; case 713: return {"bus", "BusSubmode", "schoolAndPublicServiceBus"}; case 714: return {"bus", "BusSubmode", "railReplacementBus"}; case 715: return {"bus", "BusSubmode", "demandAndResponseBus"}; case 900: return {"tram", "TramSubmode", ""}; case 901: return {"tram", "TramSubmode", "cityTram"}; case 902: return {"tram", "TramSubmode", "localTram"}; case 903: return {"tram", "TramSubmode", "regionalTram"}; case 904: return {"tram", "TramSubmode", "sightseeingTram"}; case 905: return {"tram", "TramSubmode", "shuttleTram"}; case 1000: return {"water", "", ""}; case 1100: return {"air", "", ""}; case 1300: return {"telecabin", "TelecabinSubmode", ""}; case 1301: return {"telecabin", "TelecabinSubmode", "telecabin"}; case 1302: return {"telecabin", "TelecabinSubmode", "cableCar"}; case 1303: return {"lift", "", ""}; case 1304: return {"telecabin", "TelecabinSubmode", "chairLift"}; case 1305: return {"telecabin", "TelecabinSubmode", "dragLift"}; case 1307: return {"telecabin", "TelecabinSubmode", "lift"}; case 1400: return {"funicular", "FunicularSubmode", "undefinedFunicular"}; case 1500: return {"taxi", "TaxiSubmode", ""}; case 1501: return {"taxi", "TaxiSubmode", "communalTaxi"}; case 1502: return {"taxi", "TaxiSubmode", "waterTaxi"}; case 1503: return {"taxi", "TaxiSubmode", "railTaxi"}; case 1504: return {"taxi", "TaxiSubmode", "bikeTaxi"}; case 1507: return {"taxi", "TaxiSubmode", "allTaxiServices"}; case 1700: default: return {"selfDrive", "", ""}; } } transport_mode to_pt_mode(api::ModeEnum mode) { using api::ModeEnum; switch (mode) { case ModeEnum::AIRPLANE: return {"air", "", ""}; case ModeEnum::HIGHSPEED_RAIL: return {"rail", "RailSubmode", "highSpeedRail"}; case ModeEnum::LONG_DISTANCE: return {"rail", "RailSubmode", "longDistance"}; case ModeEnum::COACH: return {"coach", "", ""}; case ModeEnum::NIGHT_RAIL: return {"rail", "RailSubmode", "nightRail"}; case ModeEnum::REGIONAL_FAST_RAIL: case ModeEnum::REGIONAL_RAIL: return {"rail", "RailSubmode", "regionalRail"}; case ModeEnum::SUBURBAN: return {"rail", "RailSubmode", "suburbanRailway"}; case ModeEnum::SUBWAY: return {"metro", "MetroSubmode", "tube"}; case ModeEnum::TRAM: return {"tram", "", ""}; case ModeEnum::BUS: return {"bus", "", ""}; case ModeEnum::FERRY: return {"water", "", ""}; case ModeEnum::ODM: return {"bus", "BusSubmode", "demandAndResponseBus"}; case ModeEnum::FUNICULAR: return {"funicular", "", ""}; case ModeEnum::AERIAL_LIFT: return {"telecabin", "", ""}; case ModeEnum::OTHER: default: return {"", "", ""}; } } template void append(std::string_view lang, pugi::xml_node node, std::string_view name, T const& value) { auto text = node.append_child(name).append_child("Text"); text.append_attribute("xml:lang").set_value(lang); text.text().set(value); } template void append(std::string_view lang, pugi::xml_node node, std::string_view name, std::optional const& value) { if (value.has_value()) { append(lang, node, name, *value); } } template void append(pugi::xml_node node, std::string_view name, T const& value) { node.append_child(name).text().set(value); } template void append(pugi::xml_node node, std::string_view name, std::optional const& value) { if (value.has_value()) { if constexpr (std::is_same_v) { node.append_child(name).text().set(value->data()); } else { node.append_child(name).text().set(*value); } } } void append_stop_ref(pugi::xml_node node, api::Place const& p) { append(node, p.parentId_ ? "siri:StopPointRef" : "siri:StopPlaceRef", p.stopId_); } void append_position(pugi::xml_node node, geo::latlng const& pos, std::string_view name = "Position"); void append_place_ref_or_geo(pugi::xml_node node, api::Place const& p) { if (p.stopId_.has_value()) { append_stop_ref(node, p); } else { append_position(node, {p.lat_, p.lon_}, "GeoPosition"); } } void append_position(pugi::xml_node node, geo::latlng const& pos, std::string_view name) { auto geo = node.append_child(name); geo.append_child("siri:Longitude").text().set(pos.lng_, 6); geo.append_child("siri:Latitude").text().set(pos.lat_, 7); } std::string_view get_place_ref(pugi::xml_node ref) { if (auto a = ref.child("StopPlaceRef")) { return a.text().as_string(); } if (auto b = ref.child("siri:StopPlaceRef")) { return b.text().as_string(); } if (auto c = ref.child("StopPointRef")) { return c.text().as_string(); } if (auto d = ref.child("siri:StopPointRef")) { return d.text().as_string(); } return ""; } std::string get_place_ref_or_geo(pugi::xml_node ref) { if (auto const id = get_place_ref(ref); !id.empty()) { return std::string{id}; } auto const geo = ref.child("GeoPosition"); utl::verify(geo, "PlaceRef.StopPlaceRef or PlaceRef.GeoPosition should be set"); auto const lat_node = geo.child("siri:Latitude"); auto const lon_node = geo.child("siri:Longitude"); utl::verify( lat_node && lon_node, "StopPlaceRef.GeoPosition needs siri:Latitude and siri:Longitude"); auto const lat = lat_node.text().as_double(); auto const lng = lon_node.text().as_double(); return fmt::format("{},{}", lat, lng); } pugi::xml_node append_mode(pugi::xml_node service, transport_mode const m) { auto const [transport_mode, submode_type, submode] = m; auto mode = service.append_child("Mode"); append(mode, "PtMode", transport_mode); if (!submode_type.empty() && !submode.empty()) { append(mode, fmt::format("siri:{}", submode_type), submode); } return mode; } std::string to_upper_ascii(std::string_view input) { auto out = std::string{input}; std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); return out; } std::string now_timestamp() { auto const now = std::chrono::system_clock::now(); return date::format("%FT%TZ", date::floor(now)); } std::string format_coord_param(double const lat, double const lon) { auto out = std::ostringstream{}; out.setf(std::ios::fixed); out << std::setprecision(6) << lat << "," << lon; return out.str(); } std::string time_to_iso(openapi::date_time_t const& t) { auto out = std::ostringstream{}; out << t; return out.str(); } std::string duration_to_iso(std::chrono::seconds const dur) { auto s = dur.count(); auto const h = s / 3600; s -= h * 3600; auto const m = s / 60; s -= m * 60; return fmt::format("PT{}{}{}", // h ? fmt::format("{}H", h) : "", m ? fmt::format("{}M", m) : "", s ? fmt::format("{}S", s) : ""); } std::string xml_to_str(pugi::xml_document const& doc) { auto out = std::ostringstream{}; doc.save(out, " ", pugi::format_indent); auto result = out.str(); if (!result.empty() && result.back() == '\n') { result.pop_back(); } return result; } std::pair create_ojp_response() { auto doc = pugi::xml_document{}; auto decl = doc.append_child(pugi::node_declaration); decl.append_attribute("version").set_value("1.0"); decl.append_attribute("encoding").set_value("utf-8"); auto ojp = doc.append_child("OJP"); ojp.append_attribute("xmlns:siri").set_value("http://www.siri.org.uk/siri"); ojp.append_attribute("xmlns").set_value("http://www.vdv.de/ojp"); ojp.append_attribute("version").set_value("2.0"); auto response = ojp.append_child("OJPResponse"); auto service_delivery = response.append_child("siri:ServiceDelivery"); append(service_delivery, "siri:ResponseTimestamp", now_timestamp()); append(service_delivery, "siri:ProducerRef", "MOTIS"); append(service_delivery, "siri:ResponseMessageIdentifier", ++response_id); return {std::move(doc), service_delivery}; } pugi::xml_document build_geocode_response( std::string_view language, api::geocode_response const& matches) { auto [doc, service_delivery] = create_ojp_response(); auto location_information = service_delivery.append_child("OJPLocationInformationDelivery"); append(location_information, "siri:ResponseTimestamp", now_timestamp()); append(location_information, "siri:DefaultLanguage", language); for (auto const& match : matches) { auto const stop_ref = match.id_; auto const name = match.name_; auto place_result = location_information.append_child("PlaceResult"); auto place = place_result.append_child("Place"); auto stop_place = place.append_child("StopPlace"); stop_place.append_child("StopPlaceRef").text().set(stop_ref); append(language, stop_place, "StopPlaceName", name); { auto const private_code = stop_place.append_child("PrivateCode"); append(private_code, "System", "EFA"); append(private_code, "Value", stop_ref); } append(place, "TopographicPlaceRef", "n/a"); append(language, place, "Name", name); append_position(place, {match.lat_, match.lon_}, "GeoPosition"); if (match.modes_.has_value()) { for (auto const& m : *match.modes_) { append_mode(place, to_pt_mode(m)); } } place_result.append_child("Complete").text().set(true); place_result.append_child("Probability").text().set(1); } return std::move(doc); } pugi::xml_document build_map_stops_response( std::string_view timestamp, std::string_view language, std::vector const& stops) { auto [doc, service_delivery] = create_ojp_response(); auto loc_delivery = service_delivery.append_child("OJPLocationInformationDelivery"); loc_delivery.append_child("siri:ResponseTimestamp") .text() .set(timestamp.data()); loc_delivery.append_child("siri:DefaultLanguage").text().set(language); for (auto const& stop : stops) { auto place_result = loc_delivery.append_child("PlaceResult"); auto place = place_result.append_child("Place"); if (stop.stopId_.has_value()) { auto stop_place = place.append_child("StopPlace"); stop_place.append_child("StopPlaceRef").text().set(*stop.stopId_); auto stop_place_name = stop_place.append_child("StopPlaceName"); auto stop_place_text = stop_place_name.append_child("Text"); stop_place_text.append_attribute("xml:lang").set_value(language); stop_place_text.text().set(stop.name_); } auto place_text = place.append_child("Name").append_child("Text"); place_text.append_attribute("xml:lang").set_value(language); place_text.text().set(stop.name_); append_position(place, {stop.lat_, stop.lon_}, "GeoPosition"); if (stop.modes_.has_value()) { for (auto const& m : *stop.modes_) { append_mode(place, to_pt_mode(m)); } } place_result.append_child("Complete").text().set(true); place_result.append_child("Probability").text().set(1); } return std::move(doc); } void add_place(auto const& t, hash_set& already_added, std::string_view language, n::lang_t const& lang, pugi::xml_node places_node, api::Place const& p) { auto const unique_id = p.stopId_.value_or(fmt::format("{},{}", p.lat_, p.lon_)); if (!already_added.insert(unique_id).second) { return; } if (p.parentId_.has_value() && p.parentId_ != p.stopId_) { add_place(t, already_added, language, lang, places_node, to_place(maybe_ref(t.tt_), maybe_ref(t.tags_), t.w_, t.pl_, t.matches_, t.ae_, t.tz_, lang, tt_location{maybe_deref(t.tags_).get_location( maybe_deref(t.tt_), *p.parentId_)})); } auto place = places_node.append_child("Place"); if (p.parentId_.has_value()) { auto sp = place.append_child("StopPoint"); append(sp, "siri:StopPointRef", p.stopId_.value()); append(language, sp, "StopPointName", p.name_); if (p.parentId_.has_value()) { append(sp, "ParentRef", *p.parentId_); } } else { auto sp = place.append_child("StopPlace"); append(sp, "siri:StopPlaceRef", p.stopId_.value()); append(language, sp, "StopPlaceName", p.name_); if (p.parentId_.has_value()) { append(sp, "ParentRef", *p.parentId_); } } append(language, place, "Name", p.name_); append_position(place, {p.lat_, p.lon_}, "GeoPosition"); } void append_leg_places(auto const& t, hash_set& already_added, std::string_view language, n::lang_t const& lang, pugi::xml_node places_node, api::Leg const& leg, bool const include_calls) { for (auto const& stop : {leg.from_, leg.to_}) { if (stop.stopId_.has_value()) { add_place(t, already_added, language, lang, places_node, stop); } } if (include_calls && leg.intermediateStops_.has_value()) { for (auto const& stop : *leg.intermediateStops_) { add_place(t, already_added, language, lang, places_node, stop); } } } pugi::xml_document build_trip_info_response(trip const& trip_ep, std::string_view language, std::string_view operating_day, std::string_view journey_ref, api::Itinerary const& itinerary, bool const include_calls, bool const include_service, bool const include_track, bool const include_places, bool const include_situations) { auto [doc, service_delivery] = create_ojp_response(); auto delivery = service_delivery.append_child("OJPTripInfoDelivery"); delivery.append_child("siri:ResponseTimestamp").text().set(now_timestamp()); delivery.append_child("siri:DefaultLanguage").text().set(language.data()); auto const& leg = itinerary.legs_.at(0); auto const lang = n::lang_t{{std::string{language}}}; if (include_places || include_situations) { auto ctx = delivery.append_child("TripInfoResponseContext"); if (include_places) { auto already_added = hash_set{}; auto places_node = ctx.append_child("Places"); append_leg_places(trip_ep, already_added, language, lang, places_node, leg, include_calls); } if (include_situations) { ctx.append_child("Situations"); } } auto result = delivery.append_child("TripInfoResult"); if (include_calls) { auto add_call = [&, n = 0](api::Place const& place) mutable { auto c = result.append_child("PreviousCall"); append_stop_ref(c, place); append(language, c, "StopPointName", place.name_); append(language, c, "PlannedQuay", place.scheduledTrack_); append(language, c, "NameSuffix", "PLATFORM_ACCESS_WITHOUT_ASSISTANCE"); // TODO real data auto arr = c.append_child("ServiceArrival"); append(arr, "TimetabledTime", place.scheduledArrival_.transform(time_to_iso)); append(arr, "EstimatedTime", place.arrival_.transform(time_to_iso)); auto dep = c.append_child("ServiceDeparture"); append(dep, "TimetabledTime", place.scheduledDeparture_.transform(time_to_iso)); append(dep, "EstimatedTime", place.departure_.transform(time_to_iso)); c.append_child("Order").text().set(++n); }; add_call(leg.from_); for (auto const& stop : leg.intermediateStops_.value()) { add_call(stop); } add_call(leg.to_); } if (include_service) { auto service = result.append_child("Service"); append(service, "OperatingDayRef", operating_day); append(service, "JourneyRef", journey_ref); append(service, "PublicCode", leg.routeShortName_); append(service, "siri:LineRef", leg.routeId_); append(service, "siri:DirectionRef", leg.directionId_); auto mode = append_mode(service, get_transport_mode(leg.routeType_.value())); append(language, mode, "Name", leg.category_.transform([](auto&& x) { return x.name_; })); append(language, mode, "ShortName", leg.category_.transform([](auto&& x) { return x.shortName_; })); append(language, service, "PublishedServiceName", leg.displayName_.value()); append(service, "TrainNumber", leg.tripShortName_); append(language, service, "OriginText", leg.tripFrom_.value().name_); append(service, "siri:OperatorRef", leg.agencyId_); append( service, "DestinationStopPointRef", leg.tripTo_.transform([](api::Place const& to) { return to.stopId_; }) .value()); append(language, service, "DestinationText", leg.headsign_); } if (include_track) { auto section = result.append_child("JourneyTrack").append_child("TrackSection"); auto start = section.append_child("TrackSectionStart"); append_stop_ref(start, leg.from_); append(language, start, "Name", leg.from_.name_); auto end = section.append_child("TrackSectionEnd"); append_stop_ref(end, leg.to_); append(language, end, "Name", leg.to_.name_); auto link = section.append_child("LinkProjection"); for (auto const& pos : geo::decode_polyline<6>(leg.legGeometry_.points_)) { append_position(link, pos); } append(section, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); append(section, "Length", leg.distance_.value_or(0.0)); } return std::move(doc); } pugi::xml_document build_stop_event_response( stop_times const& stop_times_ep, std::string_view language, bool const include_previous_calls, bool const include_onward_calls, bool const include_situations, api::stoptimes_response const& stop_times_res) { auto [doc, service_delivery] = create_ojp_response(); auto delivery = service_delivery.append_child("OJPStopEventDelivery"); delivery.append_child("siri:ResponseTimestamp").text().set(now_timestamp()); delivery.append_child("siri:DefaultLanguage").text().set(language.data()); auto const lang = n::lang_t{{std::string{language}}}; { auto ctx = delivery.append_child("StopEventResponseContext"); auto places_node = ctx.append_child("Places"); auto added = hash_set{}; for (auto const& st : stop_times_res.stopTimes_) { add_place(stop_times_ep, added, language, lang, places_node, st.place_); if (include_previous_calls && st.previousStops_.has_value()) { for (auto const& p : *st.previousStops_) { add_place(stop_times_ep, added, language, lang, places_node, p); } } if (include_onward_calls && st.nextStops_.has_value()) { for (auto const& p : *st.nextStops_) { add_place(stop_times_ep, added, language, lang, places_node, p); } } } if (include_situations) { ctx.append_child("Situations"); } } auto idx = 0; for (auto const& st : stop_times_res.stopTimes_) { auto result = delivery.append_child("StopEventResult"); append(result, "Id", ++idx); auto add_call = [&, order = 0](pugi::xml_node parent, api::Place const& place) mutable { auto call = parent.append_child("CallAtStop"); append_stop_ref(call, place); append(call, "StopPointName", place.name_); if (place.scheduledTrack_.has_value()) { append(call, "PlannedQuay", place.scheduledTrack_); } if (place.scheduledArrival_ || place.arrival_) { auto arr = call.append_child("ServiceArrival"); append(arr, "TimetabledTime", place.scheduledArrival_.transform(time_to_iso)); append(arr, "EstimatedTime", place.arrival_.transform(time_to_iso)); } if (place.scheduledDeparture_ || place.departure_) { auto dep = call.append_child("ServiceDeparture"); append(dep, "TimetabledTime", place.scheduledDeparture_.transform(time_to_iso)); append(dep, "EstimatedTime", place.departure_.transform(time_to_iso)); } append(call, "Order", ++order); }; auto stop_event = result.append_child("StopEvent"); if (include_previous_calls && st.previousStops_.has_value()) { for (auto const& p : *st.previousStops_) { add_call(stop_event.append_child("PreviousCall"), p); } } add_call(stop_event.append_child("ThisCall"), st.place_); if (include_onward_calls && st.nextStops_.has_value()) { for (auto const& p : *st.nextStops_) { add_call(stop_event.append_child("OnwardCall"), p); } } auto service = stop_event.append_child("Service"); auto const trip_id = split_trip_id(st.tripId_); append(service, "OperatingDayRef", trip_id.start_date_); append(service, "JourneyRef", st.tripId_); auto const public_code = !st.routeShortName_.empty() ? st.routeShortName_ : (!st.displayName_.empty() ? st.displayName_ : st.routeLongName_); if (!public_code.empty()) { append(service, "PublicCode", public_code); } append(service, "PublicCode", st.routeShortName_); append(service, "siri:LineRef", st.routeId_); append(service, "siri:DirectionRef", st.directionId_); auto mode = append_mode( service, st.routeType_.has_value() ? get_transport_mode(static_cast(*st.routeType_)) : to_pt_mode(st.mode_)); append(language, mode, "Name", st.displayName_); append(language, mode, "ShortName", st.routeShortName_); append(language, service, "PublishedServiceName", st.displayName_); append(service, "TrainNumber", st.tripShortName_); append(language, service, "OriginText", st.previousStops_.and_then([](std::vector const& x) { return x.empty() ? std::nullopt : std::optional{x.front().name_}; })); append(service, "siri:OperatorRef", st.agencyId_); append(language, service, "DestinationText", st.headsign_); } return std::move(doc); } pugi::xml_document build_trip_response(routing const& routing_ep, std::string_view language, api::plan_response const& plan_res, bool const include_track_sections, bool const include_leg_projection, bool const include_intermediate_stops) { auto [doc, service_delivery] = create_ojp_response(); auto delivery = service_delivery.append_child("OJPTripDelivery"); append(delivery, "siri:ResponseTimestamp", now_timestamp()); append(delivery, "siri:DefaultLanguage", language.data()); auto const lang = n::lang_t{{std::string{language}}}; auto ctx = delivery.append_child("TripResponseContext"); auto places_node = ctx.append_child("Places"); auto added = hash_set{}; for (auto const& it : plan_res.itineraries_) { for (auto const& leg : it.legs_) { append_leg_places(routing_ep, added, language, lang, places_node, leg, include_intermediate_stops); } } auto trip_idx = 0; for (auto const& it : plan_res.itineraries_) { auto const id = ++trip_idx; auto result = delivery.append_child("TripResult"); result.append_child("Id").text().set(id); auto trip = result.append_child("Trip"); append(trip, "Id", id); append(trip, "Duration", duration_to_iso(std::chrono::seconds{it.duration_})); append(trip, "StartTime", time_to_iso(it.startTime_)); append(trip, "EndTime", time_to_iso(it.endTime_)); append(trip, "Transfers", it.transfers_); append( trip, "Distance", sr::fold_left(it.legs_, 0.0, [](double const sum, api::Leg const& l) { return sum + l.legGeometry_.length_; })); auto leg_idx = 0; auto leg_pos = 0U; auto const leg_count = it.legs_.size(); api::Leg const* prev = nullptr; for (auto const& leg : it.legs_) { if (leg.displayName_.has_value()) { if (leg.interlineWithPreviousLeg_.value_or(false) && prev != nullptr) { auto leg_node = trip.append_child("Leg"); append(leg_node, "Id", ++leg_idx); append(leg_node, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); auto transfer_leg = leg_node.append_child("TransferLeg"); append(transfer_leg, "TransferType", "remainInVehicle"); auto leg_start = transfer_leg.append_child("LegStart"); append_stop_ref(leg_start, prev->to_); append(language, leg_start, "Name", prev->to_.name_); auto leg_end = transfer_leg.append_child("LegEnd"); append_stop_ref(leg_end, leg.from_); append(language, leg_end, "Name", leg.from_.name_); append(transfer_leg, "Duration", duration_to_iso(leg.startTime_.time_ - prev->endTime_.time_)); auto const co2 = leg_node.append_child("EmissionCO2"); append(co2, "KilogramPerPersonKm", 0); } auto leg_node = trip.append_child("Leg"); append(leg_node, "Id", ++leg_idx); append(leg_node, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); auto add_call = [&, order = 0](pugi::xml_node parent, api::Place const& place) mutable { append_stop_ref(parent, place); append(language, parent, "StopPointName", place.name_); if (place.scheduledTrack_.has_value()) { append(language, parent, "PlannedQuay", *place.scheduledTrack_); } if (place.departure_) { auto dep = parent.append_child("ServiceDeparture"); append(dep, "TimetabledTime", place.scheduledDeparture_.transform(time_to_iso)); append(dep, "EstimatedTime", place.departure_.transform(time_to_iso)); } if (place.arrival_) { auto arr = parent.append_child("ServiceArrival"); append(arr, "TimetabledTime", place.scheduledArrival_.transform(time_to_iso)); append(arr, "EstimatedTime", place.arrival_.transform(time_to_iso)); } parent.append_child("Order").text().set(++order); }; auto timed_leg = leg_node.append_child("TimedLeg"); add_call(timed_leg.append_child("LegBoard"), leg.from_); for (auto const& stop : *leg.intermediateStops_) { add_call(timed_leg.append_child("LegIntermediate"), stop); } add_call(timed_leg.append_child("LegAlight"), leg.to_); auto service = timed_leg.append_child("Service"); auto const [start_day, _, _1, _2] = split_trip_id(leg.tripId_.value()); append(service, "OperatingDayRef", start_day); append(service, "JourneyRef", leg.tripId_); append(service, "LineRef", leg.routeId_); append(service, "DirectionRef", leg.directionId_); append(service, "siri:OperatorRef", leg.agencyId_); { auto const category = service.append_child("ProductCategory"); append(language, category, "Name", leg.category_.transform([](auto&& x) { return x.name_; })); append( language, category, "ShortName", leg.category_.transform([](auto&& x) { return x.shortName_; })); append(language, category, "ProductCategoryRef", leg.category_.transform([](auto&& x) { return x.id_; })); } append(language, service, "DestinationText", leg.headsign_); // TODO // // // Niederflureinstieg // // A__NF // 50 // append(language, service, "PublishedServiceName", leg.displayName_); { auto mode = append_mode(service, get_transport_mode(leg.routeType_.value())); append(language, mode, "Name", leg.category_.transform([](auto&& x) { return x.name_; })); append( language, mode, "ShortName", leg.category_.transform([](auto&& x) { return x.shortName_; })); } if (include_track_sections || include_leg_projection) { auto leg_track = timed_leg.append_child("LegTrack"); auto section = leg_track.append_child("TrackSection"); auto start = section.append_child("TrackSectionStart"); append_stop_ref(start, leg.from_); append(language, start, "Name", leg.from_.name_); auto end = section.append_child("TrackSectionEnd"); append_stop_ref(start, leg.to_); append(language, end, "Name", leg.to_.name_); if (include_leg_projection) { auto link = section.append_child("LinkProjection"); for (auto const& pos : geo::decode_polyline<6>(leg.legGeometry_.points_)) { append_position(link, pos); } } append(section, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); append(section, "Length", leg.legGeometry_.length_); } } else { auto const is_first_or_last = (leg_pos == 0U) || (leg_pos + 1U == leg_count); auto leg_node = trip.append_child("Leg"); append(leg_node, "Id", ++leg_idx); append(leg_node, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); if (is_first_or_last) { auto continuous_leg = leg_node.append_child("ContinuousLeg"); auto leg_start = continuous_leg.append_child("LegStart"); append_place_ref_or_geo(leg_start, leg.from_); append(language, leg_start, "Name", leg.from_.name_); auto leg_end = continuous_leg.append_child("LegEnd"); append_place_ref_or_geo(leg_end, leg.to_); append(language, leg_end, "Name", leg.to_.name_); auto service = continuous_leg.append_child("Service"); append(service, "PersonalModeOfOperation", "own"); append(service, "PersonalMode", "foot"); append(continuous_leg, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); append(continuous_leg, "Length", leg.legGeometry_.length_); auto leg_track = continuous_leg.append_child("LegTrack"); auto track_section = leg_track.append_child("TrackSection"); auto track_section_start = track_section.append_child("TrackSectionStart"); append_place_ref_or_geo(track_section_start, leg.from_); append(language, track_section_start, "Name", leg.from_.name_); auto track_section_end = track_section.append_child("TrackSectionEnd"); append_place_ref_or_geo(track_section_end, leg.to_); append(language, track_section_end, "Name", leg.to_.name_); auto link_projection = track_section.append_child("LinkProjection"); for (auto const& pos : geo::decode_polyline<6>(leg.legGeometry_.points_)) { append_position(link_projection, pos); } append(track_section, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); append(track_section, "Length", leg.legGeometry_.length_); } else { auto transfer_leg = leg_node.append_child("TransferLeg"); append(transfer_leg, "TransferType", "walk"); auto leg_start = transfer_leg.append_child("LegStart"); append_place_ref_or_geo(leg_start, leg.from_); append(language, leg_start, "Name", leg.from_.name_); auto leg_end = transfer_leg.append_child("LegEnd"); append_place_ref_or_geo(leg_end, leg.to_); append(language, leg_end, "Name", leg.to_.name_); append(transfer_leg, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); auto path_guidance = transfer_leg.append_child("PathGuidance"); auto track_section = path_guidance.append_child("TrackSection"); auto track_section_start = track_section.append_child("TrackSectionStart"); append_place_ref_or_geo(track_section_start, leg.from_); append(language, track_section_start, "Name", leg.from_.name_); auto track_section_end = track_section.append_child("TrackSectionEnd"); append_place_ref_or_geo(track_section_end, leg.to_); append(language, track_section_end, "Name", leg.to_.name_); auto link_projection = track_section.append_child("LinkProjection"); for (auto const& pos : geo::decode_polyline<6>(leg.legGeometry_.points_)) { append_position(link_projection, pos); } append(track_section, "Duration", duration_to_iso(std::chrono::seconds{leg.duration_})); append(track_section, "Length", leg.legGeometry_.length_); } } prev = ⋚ ++leg_pos; } } return std::move(doc); } net::reply ojp::operator()(net::route_request const& http_req, bool) const { auto xml = pugi::xml_document{}; xml.load_string(http_req.body().c_str()); auto const req = xml.child("OJP").child("OJPRequest").child("siri:ServiceRequest"); utl::verify( req, "no OJPReuqest > siri:ServiceRequest found"); auto const context = req.child("siri:ServiceRequestContext"); auto const language = context.child("siri:Language").text().as_string(); auto const lang = language ? language : std::string{"en"}; auto response = pugi::xml_document{}; if (auto const loc_req = req.child("OJPLocationInformationRequest"); loc_req) { auto const input = loc_req.child("InitialInput"); if (auto const geo = input.child("GeoRestriction"); geo) { utl::verify(stops_ep_.has_value(), "stops not loaded"); auto const rect = geo.child("Rectangle"); auto const upper_left = rect.child("UpperLeft"); auto const lower_right = rect.child("LowerRight"); utl::verify(upper_left && lower_right, "missing GeoRestriction box"); auto url = boost::urls::url{"/api/v1/map/stop"}; auto params = url.params(); params.append( {"min", fmt::format("{},{}", lower_right.child("siri:Latitude").text().as_double(), upper_left.child("siri:Longitude").text().as_double())}); params.append( {"max", fmt::format( "{},{}", upper_left.child("siri:Latitude").text().as_double(), lower_right.child("siri:Longitude").text().as_double())}); params.append({"language", lang}); response = build_map_stops_response(now_timestamp(), lang, (*stops_ep_)(url)); } else if (auto const stop_id = loc_req.child("PlaceRef") .child("StopPlaceRef") .text() .as_string(); stop_id && strlen(stop_id) != 0U) { auto const& tt = *geocoding_ep_->tt_; auto const& tags = geocoding_ep_->tags_; auto const stop = tags->get_location(tt, stop_id); auto const pos = tt.locations_.coordinates_.at(stop); response = build_geocode_response( language, std::vector{api::Match{ .type_ = api::LocationTypeEnum::STOP, .name_ = std::string{tt.get_default_translation( tt.locations_.names_.at(stop))}, .id_ = tags->id(tt, stop), .lat_ = pos.lat(), .lon_ = pos.lng(), }}); } else { auto const name = input.child("Name").text(); utl::verify(geocoding_ep_.has_value(), "geocoding not loaded"); auto url = boost::urls::url{"/api/v1/geocode"}; auto params = url.params(); params.append({"text", name.as_string()}); params.append({"language", lang}); params.append({"type", "STOP"}); response = build_geocode_response(lang, (*geocoding_ep_)(url)); } } else if (auto const trip_info_req = req.child("OJPTripInfoRequest")) { utl::verify(trip_ep_.has_value(), "trip not loaded"); auto const journey_ref = trip_info_req.child("JourneyRef").text().as_string(); auto const operating_day = trip_info_req.child("OperatingDayRef").text().as_string(); auto const params = trip_info_req.child("Params"); auto url = boost::urls::url{"/api/v5/trip"}; auto url_params = url.params(); url_params.append({"tripId", journey_ref}); url_params.append({"language", lang}); response = build_trip_info_response( *trip_ep_, lang, operating_day, journey_ref, (*trip_ep_)(url), params.child("IncludeCalls").text().as_bool(true), params.child("IncludeService").text().as_bool(true), params.child("IncludeTrackProjection").text().as_bool(true), params.child("IncludePlacesContext").text().as_bool(true), params.child("IncludeSituationsContext").text().as_bool(true)); } else if (auto const plan_req = req.child("OJPTripRequest")) { utl::verify(routing_ep_.has_value(), "routing not loaded"); auto const origin = plan_req.child("Origin"); auto const destination = plan_req.child("Destination"); auto const origin_ref = origin.child("PlaceRef"); auto const destination_ref = destination.child("PlaceRef"); auto const dep_time = std::string_view{origin.child("DepArrTime").text().as_string()}; auto const arr_time = std::string_view{destination.child("DepArrTime").text().as_string()}; auto const params = plan_req.child("Params"); auto const num_results = params.child("NumberOfResults").text().as_int(5); auto const include_track_sections = params.child("IncludeTrackSections").text().as_bool(false); auto const include_leg_projection = params.child("IncludeLegProjection").text().as_bool(false); auto const include_intermediate_stops = params.child("IncludeIntermediateStops").text().as_bool(false); auto url = boost::urls::url{"/api/v5/plan"}; auto url_params = url.params(); url_params.append({"fromPlace", get_place_ref_or_geo(origin_ref)}); url_params.append({"toPlace", get_place_ref_or_geo(destination_ref)}); url_params.append({"time", !dep_time.empty() ? dep_time : arr_time}); url_params.append({"numItineraries", fmt::format("{}", num_results)}); url_params.append({"joinInterlinedLegs", "false"}); url_params.append({"language", lang}); if (dep_time.empty()) { url_params.append({"arriveBy", "true"}); } response = build_trip_response( *routing_ep_, lang, (*routing_ep_)(url), include_track_sections, include_leg_projection, include_intermediate_stops); } else if (auto const stop_times_req = req.child("OJPStopEventRequest")) { utl::verify(stop_times_ep_.has_value(), "stop times not loaded"); auto const location = stop_times_req.child("Location"); auto const place_ref = location.child("PlaceRef"); auto const stop_id = get_place_ref(place_ref); auto const dep_arr_time = location.child("DepArrTime").text().as_string(); auto const params = stop_times_req.child("Params"); auto const number_of_results = params.child("NumberOfResults"); auto const stop_event_type = params.child("StopEventType").text().as_string(); auto const include_prev = params.child("IncludePreviousCalls").text().as_bool(false); auto const include_onward = params.child("IncludeOnwardCalls").text().as_bool(false); auto url = boost::urls::url{"/api/v5/stoptimes"}; auto url_params = url.params(); url_params.append({"stopId", stop_id}); url_params.append({"language", lang}); if (number_of_results) { url_params.append( {"n", fmt::to_string(number_of_results.text().as_int())}); } if (dep_arr_time) { url_params.append({"time", dep_arr_time}); } url_params.append({"fetchStops", "true"}); if (stop_event_type == "arrival"sv) { url_params.append({"arriveBy", "true"}); } response = build_stop_event_response(*stop_times_ep_, lang, include_prev, include_onward, true, (*stop_times_ep_)(url)); } else { throw net::bad_request_exception{"unsupported OJP request"}; } auto reply = net::web_server::string_res_t{boost::beast::http::status::ok, http_req.version()}; reply.insert(boost::beast::http::field::content_type, "text/xml; charset=utf-8"); net::set_response_body(reply, http_req, xml_to_str(response)); reply.keep_alive(http_req.keep_alive()); return reply; } } // namespace motis::ep ================================================ FILE: src/endpoints/one_to_all.cc ================================================ #include "motis/endpoints/one_to_all.h" #include #include #include "utl/verify.h" #include "net/bad_request_exception.h" #include "net/too_many_exception.h" #include "nigiri/common/delta_t.h" #include "nigiri/routing/limits.h" #include "nigiri/routing/one_to_all.h" #include "nigiri/routing/query.h" #include "nigiri/types.h" #include "motis-api/motis-api.h" #include "motis/endpoints/routing.h" #include "motis/gbfs/routing_data.h" #include "motis/metrics_registry.h" #include "motis/place.h" #include "motis/timetable/modes_to_clasz_mask.h" namespace motis::ep { namespace n = nigiri; api::Reachable one_to_all::operator()(boost::urls::url_view const& url) const { metrics_->routing_requests_.Increment(); auto const max_travel_minutes = config_.get_limits().onetoall_max_travel_minutes_; auto const query = api::oneToAll_params{url.params()}; utl::verify( query.maxTravelTime_ <= max_travel_minutes, "maxTravelTime too large ({} > {}). The server admin can change " "this limit in config.yml with 'onetoall_max_travel_minutes'. " "See documentation for details.", query.maxTravelTime_, max_travel_minutes); if (query.maxTransfers_.has_value()) { utl::verify(query.maxTransfers_ >= 0U, "maxTransfers < 0: {}", *query.maxTransfers_); utl::verify( query.maxTransfers_ <= n::routing::kMaxTransfers, "maxTransfers > {}: {}", n::routing::kMaxTransfers, *query.maxTransfers_); } auto const unreachable = query.arriveBy_ ? n::kInvalidDelta : n::kInvalidDelta; auto const make_place = [&](place_t const& p, n::unixtime_t const t, n::event_type const ev) { auto place = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, {}, p); if (ev == n::event_type::kArr) { place.arrival_ = t; } else { place.departure_ = t; } return place; }; auto const time = std::chrono::time_point_cast( *query.time_.value_or(openapi::now())); auto const max_travel_time = n::duration_t{query.maxTravelTime_}; auto const one = get_place(&tt_, &tags_, query.one_); auto const one_modes = deduplicate(query.arriveBy_ ? query.postTransitModes_ : query.preTransitModes_); auto const one_max_time = std::min( std::chrono::seconds{query.arriveBy_ ? query.maxPostTransitTime_ : query.maxPreTransitTime_}, std::min( std::chrono::duration_cast(max_travel_time), std::chrono::seconds{ config_.get_limits() .street_routing_max_prepost_transit_seconds_})); auto const one_dir = query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward; auto const r = routing{ config_, w_, l_, pl_, elevations_, &tt_, nullptr, &tags_, loc_tree_, fa_, matches_, way_matches_, rt_, nullptr, gbfs_, nullptr, nullptr, nullptr, nullptr, metrics_}; auto gbfs_rd = gbfs::gbfs_routing_data{w_, l_, gbfs_}; auto const osr_params = get_osr_parameters(query); auto prepare_stats = std::map{}; auto q = n::routing::query{ .start_time_ = time, .start_match_mode_ = get_match_mode(r, one), .start_ = r.get_offsets( nullptr, one, one_dir, one_modes, std::nullopt, std::nullopt, std::nullopt, std::nullopt, false, osr_params, query.pedestrianProfile_, query.elevationCosts_, one_max_time, query.maxMatchingDistance_, gbfs_rd, prepare_stats), .td_start_ = r.get_td_offsets( nullptr, nullptr, one, one_dir, one_modes, osr_params, query.pedestrianProfile_, query.elevationCosts_, query.maxMatchingDistance_, one_max_time, time, prepare_stats), .max_transfers_ = static_cast( query.maxTransfers_.value_or(n::routing::kMaxTransfers)), .max_travel_time_ = max_travel_time, .prf_idx_ = static_cast( query.useRoutedTransfers_ ? (query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR ? 2U : 1U) : 0U), .allowed_claszes_ = to_clasz_mask(query.transitModes_), .require_bike_transport_ = query.requireBikeTransport_, .require_car_transport_ = query.requireCarTransport_, .transfer_time_settings_ = n::routing::transfer_time_settings{ .default_ = (query.minTransferTime_ == 0 && query.additionalTransferTime_ == 0 && query.transferTimeFactor_ == 1.0), .min_transfer_time_ = n::duration_t{query.minTransferTime_}, .additional_time_ = n::duration_t{query.additionalTransferTime_}, .factor_ = static_cast(query.transferTimeFactor_)}, }; if (tt_.locations_.footpaths_out_.at(q.prf_idx_).empty()) { q.prf_idx_ = 0U; } auto const state = query.arriveBy_ ? n::routing::one_to_all(tt_, nullptr, q) : n::routing::one_to_all(tt_, nullptr, q); auto reachable = nigiri::bitvec{tt_.n_locations()}; for (auto i = 0U; i != tt_.n_locations(); ++i) { if (state.get_best<0>()[i][0] != unreachable) { reachable.set(i); } } auto const max_results = config_.get_limits().onetoall_max_results_; utl::verify(reachable.count() <= max_results, "too many results: {} > {}", reachable.count(), max_results); auto all = std::vector{}; all.reserve(reachable.count()); auto const all_ev = query.arriveBy_ ? n::event_type::kDep : n::event_type::kArr; reachable.for_each_set_bit([&](auto const i) { auto const l = n::location_idx_t{i}; auto const fastest = n::routing::get_fastest_one_to_all_offsets( tt_, state, query.arriveBy_ ? n::direction::kBackward : n::direction::kForward, l, time, q.max_transfers_); all.push_back(api::ReachablePlace{ make_place(tt_location{l}, time + std::chrono::minutes{fastest.duration_}, all_ev), query.arriveBy_ ? -fastest.duration_ : fastest.duration_, fastest.k_}); }); return { .one_ = make_place( one, time, query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep), .all_ = std::move(all), }; } } // namespace motis::ep ================================================ FILE: src/endpoints/one_to_many.cc ================================================ #include "motis/endpoints/one_to_many.h" #include #include #include #include "utl/enumerate.h" #include "net/too_many_exception.h" #include "nigiri/common/delta_t.h" #include "nigiri/routing/one_to_all.h" #include "motis/config.h" #include "motis/endpoints/one_to_many_post.h" #include "motis/endpoints/routing.h" #include "motis/gbfs/routing_data.h" #include "motis/osr/mode_to_profile.h" #include "motis/timetable/modes_to_clasz_mask.h" namespace motis::ep { namespace n = nigiri; constexpr auto const kInfinity = std::numeric_limits::infinity(); api::oneToMany_response one_to_many_direct( config const& config, osr::ways const& w, osr::lookup const& l, api::ModeEnum const mode, osr::location const& one, std::vector const& many, double const max_direct_time, double const max_matching_distance, osr::direction const dir, osr_parameters const& params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, osr::elevation_storage const* elevations_, bool const with_distance) { auto const max_many = config.get_limits().onetomany_max_many_; auto const max_direct_time_limit = config.get_limits().street_routing_max_direct_seconds_; utl::verify( many.size() <= max_many, "number of many locations too high ({} > {}). The server admin can " "change this limit in config.yml with 'onetomany_max_many'. " "See documentation for details.", many.size(), max_many); utl::verify( max_direct_time <= max_direct_time_limit, "maximun travel time too high ({} > {}). The server admin can " "change this limit in config.yml with " "'street_routing_max_direct_seconds'. " "See documentation for details.", max_direct_time, max_direct_time_limit); utl::verify( mode == api::ModeEnum::BIKE || mode == api::ModeEnum::CAR || mode == api::ModeEnum::WALK, "mode {} not supported for one-to-many", fmt::streamed(mode)); auto const profile = to_profile(mode, pedestrian_profile, elevation_costs); auto const paths = osr::route(to_profile_parameters(profile, params), w, l, profile, one, many, max_direct_time, dir, max_matching_distance, nullptr, nullptr, elevations_, [&](auto&&) { return with_distance; }); return utl::to_vec(paths, [&](std::optional const& p) { return p .transform([&](osr::path const& x) { return api::Duration{.duration_ = x.cost_, .distance_ = with_distance ? std::optional{x.dist_} : std::nullopt}; }) .value_or(api::Duration{}); }); } double duration_to_seconds(n::duration_t const d) { return 60 * d.count(); } template std::vector transit_durations( Endpoint const& ep, Query const& query, place_t const& one, std::vector const& many, auto const& time, bool const arrive_by, std::chrono::seconds const max_travel_time, double const max_matching_distance, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, osr_parameters const& osr_params) { // Code is similar to One-to-All auto const one_modes = deduplicate(arrive_by ? query.postTransitModes_ : query.preTransitModes_); auto const many_modes = deduplicate(arrive_by ? query.preTransitModes_ : query.postTransitModes_); auto const max_prepost_seconds = std::min( max_travel_time, std::chrono::seconds{ ep.config_.get_limits().street_routing_max_prepost_transit_seconds_}); auto const one_max_seconds = std::min(std::chrono::seconds{arrive_by ? query.maxPostTransitTime_ : query.maxPreTransitTime_}, max_prepost_seconds); auto const many_max_seconds = std::min(std::chrono::seconds{arrive_by ? query.maxPreTransitTime_ : query.maxPostTransitTime_}, max_prepost_seconds); auto const one_dir = arrive_by ? osr::direction::kBackward : osr::direction::kForward; auto const unreachable = arrive_by ? n::kInvalidDelta : n::kInvalidDelta; auto const r = routing{ep.config_, ep.w_, ep.l_, ep.pl_, ep.elevations_, &ep.tt_, nullptr, &ep.tags_, ep.loc_tree_, ep.fa_, ep.matches_, ep.way_matches_, ep.rt_, nullptr, ep.gbfs_, nullptr, nullptr, nullptr, nullptr, ep.metrics_}; auto gbfs_rd = gbfs::gbfs_routing_data{ep.w_, ep.l_, ep.gbfs_}; auto prepare_stats = std::map{}; auto q = n::routing::query{ .start_time_ = time, .start_match_mode_ = get_match_mode(r, one), .start_ = r.get_offsets(nullptr, one, one_dir, one_modes, std::nullopt, std::nullopt, std::nullopt, std::nullopt, false, osr_params, pedestrian_profile, elevation_costs, one_max_seconds, max_matching_distance, gbfs_rd, prepare_stats), .td_start_ = r.get_td_offsets(nullptr, nullptr, one, one_dir, one_modes, osr_params, pedestrian_profile, elevation_costs, max_matching_distance, one_max_seconds, time, prepare_stats), .max_transfers_ = static_cast( query.maxTransfers_.value_or(n::routing::kMaxTransfers)), .max_travel_time_ = std::chrono::duration_cast(max_travel_time), .prf_idx_ = query.useRoutedTransfers_ ? (query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR ? n::kWheelchairProfile : n::kFootProfile) : n::kDefaultProfile, .allowed_claszes_ = to_clasz_mask(query.transitModes_), .require_bike_transport_ = query.requireBikeTransport_, .require_car_transport_ = query.requireCarTransport_, .transfer_time_settings_ = n::routing::transfer_time_settings{ .default_ = (query.minTransferTime_ == 0 && query.additionalTransferTime_ == 0 && query.transferTimeFactor_ == 1.0), .min_transfer_time_ = n::duration_t{query.minTransferTime_}, .additional_time_ = n::duration_t{query.additionalTransferTime_}, .factor_ = static_cast(query.transferTimeFactor_)}, }; if (ep.tt_.locations_.footpaths_out_.at(q.prf_idx_).empty()) { q.prf_idx_ = n::kDefaultProfile; } // Compute and update durations using transits auto const state = arrive_by ? n::routing::one_to_all(ep.tt_, nullptr, q) : n::routing::one_to_all(ep.tt_, nullptr, q); auto reachable = n::bitvec{ep.tt_.n_locations()}; for (auto i = 0U; i != ep.tt_.n_locations(); ++i) { if (state.template get_best<0>()[i][0] != unreachable) { reachable.set(i); } } auto pareto_sets = std::vector{}; auto const dir = arrive_by ? n::direction::kBackward : n::direction::kForward; // As we iterate over all offsets first, we need to store all best solutions // If we would iterate over k first, we could build the pareto set directly. // However, that would require more work for handling internals like `delta_t` auto totals = n::vector{}; for (auto const [i, l] : utl::enumerate(many)) { totals.clear(); totals.resize(q.max_transfers_, kInfinity); auto const offsets = r.get_offsets( nullptr, l, arrive_by ? osr::direction::kForward : osr::direction::kBackward, many_modes, std::nullopt, std::nullopt, std::nullopt, std::nullopt, false, osr_params, pedestrian_profile, elevation_costs, many_max_seconds, max_matching_distance, gbfs_rd, prepare_stats); for (auto const offset : offsets) { auto const loc = offset.target(); if (reachable.test(to_idx(loc))) { auto const base = duration_to_seconds(offset.duration()); n::routing::for_each_one_to_all_round_time( ep.tt_, state, dir, loc, time, q.max_transfers_, [&](std::uint8_t const k, n::duration_t const d) { if (k != std::uint8_t{0U}) { auto const total = base + duration_to_seconds(d); totals[k - 1U] = std::min(totals[k - 1], total); } }); } } auto entries = api::ParetoSet{}; auto best = kInfinity; for (auto const [t, d] : utl::enumerate(totals)) { // Filter long durations from offsets with many required transfers if (d < best) { entries.emplace_back(d, t); best = d; } } pareto_sets.emplace_back(std::move(entries)); } return pareto_sets; } api::oneToMany_response one_to_many::operator()( boost::urls::url_view const& url) const { auto const query = api::oneToMany_params{url.params()}; return one_to_many_handle_request(config_, query, w_, l_, elevations_, metrics_); } template api::OneToManyIntermodalResponse run_one_to_many_intermodal( Endpoint const& ep, Query const& query, place_t const& one, std::vector const& many) { ep.metrics_->one_to_many_requests_.Increment(); auto const time = std::chrono::time_point_cast( *query.time_.value_or(openapi::now())); auto const max_travel_time = query.maxTravelTime_ .transform([](std::int64_t const dur) { using namespace std::chrono; return duration_cast(minutes{dur}); }) .value_or(n::routing::kMaxTravelTime); auto const osr_params = get_osr_parameters(query); // Get street routing durations auto const to_location = [&](place_t const& p) { return get_location(&ep.tt_, ep.w_, ep.pl_, ep.matches_, p); }; return {.street_durations_ = one_to_many_direct( ep.config_, *ep.w_, *ep.l_, query.directMode_, to_location(one), utl::to_vec(many, to_location), static_cast(std::min( {std::max({query.maxDirectTime_, query.maxPreTransitTime_, query.maxPostTransitTime_}), static_cast(max_travel_time.count()), static_cast( ep.config_.get_limits() .street_routing_max_direct_seconds_)})), query.maxMatchingDistance_, query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward, osr_params, query.pedestrianProfile_, query.elevationCosts_, ep.elevations_, query.withDistance_), .transit_durations_ = transit_durations( ep, query, one, many, time, query.arriveBy_, max_travel_time, query.maxMatchingDistance_, query.pedestrianProfile_, query.elevationCosts_, osr_params)}; } api::OneToManyIntermodalResponse one_to_many_intermodal::operator()( boost::urls::url_view const& url) const { auto const query = api::oneToManyIntermodal_params{url.params()}; auto const one = parse_location(query.one_, ';'); utl::verify( one.has_value(), "{} is not a valid geo coordinate", query.one_); auto const many = utl::to_vec(query.many_, [](auto&& x) -> place_t { auto const y = parse_location(x, ';'); utl::verify( y.has_value(), "{} is not a valid geo coordinate", x); return *y; }); return run_one_to_many_intermodal(*this, query, *one, many); } template api::OneToManyIntermodalResponse run_one_to_many_intermodal< one_to_many_intermodal, api::oneToManyIntermodal_params>(one_to_many_intermodal const&, api::oneToManyIntermodal_params const&, place_t const&, std::vector const&); template api::OneToManyIntermodalResponse run_one_to_many_intermodal< one_to_many_intermodal_post, api::OneToManyIntermodalParams>(one_to_many_intermodal_post const&, api::OneToManyIntermodalParams const&, place_t const&, std::vector const&); } // namespace motis::ep ================================================ FILE: src/endpoints/one_to_many_post.cc ================================================ #include "motis/endpoints/one_to_many_post.h" #include #include "utl/to_vec.h" #include "motis/endpoints/one_to_many.h" #include "motis/place.h" namespace motis::ep { api::oneToManyPost_response one_to_many_post::operator()( api::OneToManyParams const& query) const { return one_to_many_handle_request(config_, query, w_, l_, elevations_, metrics_); } api::OneToManyIntermodalResponse one_to_many_intermodal_post::operator()( api::OneToManyIntermodalParams const& query) const { auto const one = get_place(&tt_, &tags_, query.one_); auto const many = utl::to_vec(query.many_, [&](std::string_view place) -> place_t { return get_place(&tt_, &tags_, place); }); return run_one_to_many_intermodal(*this, query, one, many); } } // namespace motis::ep ================================================ FILE: src/endpoints/osr_routing.cc ================================================ #include "motis/endpoints/osr_routing.h" #include "utl/pipes.h" #include "osr/geojson.h" #include "osr/routing/route.h" #include "motis/data.h" #include "motis/osr/parameters.h" namespace json = boost::json; namespace motis::ep { osr::location parse_location(json::value const& v) { auto const& obj = v.as_object(); return {{obj.at("lat").as_double(), obj.at("lng").as_double()}, obj.contains("level") ? osr::level_t{obj.at("level").to_number()} : osr::kNoLevel}; } json::value osr_routing::operator()(json::value const& query) const { auto const rt = std::atomic_load(&rt_); auto const e = rt->e_.get(); auto const& q = query.as_object(); auto const profile_it = q.find("profile"); auto const profile = osr::to_profile(profile_it == q.end() || !profile_it->value().is_string() ? to_str(osr::search_profile::kFoot) : profile_it->value().as_string()); auto const direction_it = q.find("direction"); auto const dir = osr::to_direction(direction_it == q.end() || !direction_it->value().is_string() ? to_str(osr::direction::kForward) : direction_it->value().as_string()); auto const from = parse_location(q.at("start")); auto const to = parse_location(q.at("destination")); auto const max_it = q.find("max"); auto const max = static_cast( max_it == q.end() ? 3600 : max_it->value().as_int64()); auto const p = route(to_profile_parameters(profile, {}), w_, l_, profile, from, to, max, dir, 8, e == nullptr ? nullptr : &e->blocked_); return p.has_value() ? json::value{{"type", "FeatureCollection"}, {"metadata", {{"duration", p->cost_}, {"distance", p->dist_}, {"uses_elevator", p->uses_elevator_}}}, {"features", utl::all(p->segments_) // | utl::transform([&](auto&& s) { return json::value{ {"type", "Feature"}, {"properties", {{"level", s.from_level_.to_float()}, {"way", s.way_ == osr::way_idx_t::invalid() ? 0U : to_idx( w_.way_osm_idx_[s.way_])}}}, {"geometry", osr::to_line_string(s.polyline_)}}; }) // | utl::emplace_back_to()}} : json::value{{"error", "no path found"}}; } } // namespace motis::ep ================================================ FILE: src/endpoints/platforms.cc ================================================ #include "motis/endpoints/platforms.h" #include "net/too_many_exception.h" #include "osr/geojson.h" namespace json = boost::json; namespace motis::ep { constexpr auto const kLimit = 4096U; json::value platforms::operator()(json::value const& query) const { auto const& q = query.as_object(); auto const level = q.contains("level") ? osr::level_t{query.at("level").to_number()} : osr::kNoLevel; auto const waypoints = q.at("waypoints").as_array(); auto const min = osr::point::from_latlng( {waypoints[1].as_double(), waypoints[0].as_double()}); auto const max = osr::point::from_latlng( {waypoints[3].as_double(), waypoints[2].as_double()}); auto gj = osr::geojson_writer{.w_ = w_, .platforms_ = &pl_}; pl_.find(min, max, [&](osr::platform_idx_t const i) { if (level == osr::kNoLevel || pl_.get_level(w_, i) == level) { utl::verify(gj.features_.size() < kLimit, "too many platforms"); gj.write_platform(i); } }); return gj.json(); } } // namespace motis::ep ================================================ FILE: src/endpoints/routing.cc ================================================ #include "motis/endpoints/routing.h" #include #include #include "boost/thread/tss.hpp" #include "net/bad_request_exception.h" #include "net/too_many_exception.h" #include "prometheus/counter.h" #include "prometheus/histogram.h" #include "utl/erase_duplicates.h" #include "utl/helpers/algorithm.h" #include "utl/timing.h" #include "osr/lookup.h" #include "osr/platforms.h" #include "osr/routing/profile.h" #include "osr/routing/route.h" #include "osr/routing/sharing_data.h" #include "osr/types.h" #include "nigiri/common/interval.h" #include "nigiri/location_match_mode.h" #include "nigiri/routing/limits.h" #include "nigiri/routing/pareto_set.h" #include "nigiri/routing/query.h" #include "nigiri/routing/raptor/raptor_state.h" #include "nigiri/routing/raptor_search.h" #include "nigiri/routing/raptor/pong.h" #include "nigiri/routing/tb/query_engine.h" #include "nigiri/routing/tb/tb_data.h" #include "nigiri/routing/tb/tb_search.h" #include "motis/config.h" #include "motis/constants.h" #include "motis/direct_filter.h" #include "motis/endpoints/routing.h" #include "motis/flex/flex.h" #include "motis/flex/flex_output.h" #include "motis/gbfs/data.h" #include "motis/gbfs/gbfs_output.h" #include "motis/gbfs/mode.h" #include "motis/gbfs/osr_profile.h" #include "motis/get_stops_with_traffic.h" #include "motis/journey_to_response.h" #include "motis/match_platforms.h" #include "motis/metrics_registry.h" #include "motis/odm/meta_router.h" #include "motis/osr/max_distance.h" #include "motis/osr/mode_to_profile.h" #include "motis/osr/street_routing.h" #include "motis/parse_location.h" #include "motis/server.h" #include "motis/tag_lookup.h" #include "motis/timetable/modes_to_clasz_mask.h" #include "motis/timetable/time_conv.h" #include "motis/update_rtt_td_footpaths.h" namespace n = nigiri; using namespace std::chrono_literals; namespace motis::ep { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) boost::thread_specific_ptr> blocked; bool osr_loaded(routing const& r) { return r.w_ && r.l_ && r.pl_ && r.tt_ && r.loc_tree_ && r.matches_; } bool is_intermodal(routing const& r, place_t const&) { return osr_loaded(r); // use pre/post transit even when start/dest is a stop } n::routing::location_match_mode get_match_mode(routing const& r, place_t const& p) { return is_intermodal(r, p) ? n::routing::location_match_mode::kIntermodal : n::routing::location_match_mode::kEquivalent; } std::vector station_start(n::location_idx_t const l) { return {{l, n::duration_t{0U}, 0U}}; } std::vector radius_offsets( point_rtree const& loc_tree, geo::latlng const& pos, double const radius_meters) { auto offsets = std::vector{}; loc_tree.in_radius(pos, radius_meters, [&](n::location_idx_t const l) { offsets.push_back({l, n::duration_t{0U}, 0U}); }); return offsets; } osr::location stop_to_osr_location(routing const& r, n::location_idx_t const l) { return osr::location{r.tt_->locations_.coordinates_[l], r.pl_->get_level(*r.w_, (*r.matches_)[l])}; } n::routing::td_offsets_t get_td_offsets( routing const& r, n::rt_timetable const* rtt, elevators const* e, osr::location const& pos, osr::direction const dir, std::vector const& modes, osr_parameters const& osr_params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, double const max_matching_distance, std::chrono::seconds const max, nigiri::routing::start_time_t const& start_time, stats_map_t& stats) { if (!osr_loaded(r)) { return {}; } auto ret = hash_map>{}; for (auto const m : modes) { if (m == api::ModeEnum::ODM || m == api::ModeEnum::RIDE_SHARING) { continue; } else if (m == api::ModeEnum::FLEX) { UTL_START_TIMING(flex_timer); utl::verify(r.fa_, "FLEX areas not loaded"); auto frd = flex::flex_routing_data{}; flex::add_flex_td_offsets(*r.w_, *r.l_, r.pl_, r.matches_, r.way_matches_, *r.tt_, *r.fa_, *r.loc_tree_, start_time, pos, dir, max, max_matching_distance, osr_params, frd, ret, stats); stats.emplace(fmt::format("prepare_{}_FLEX", to_str(dir)), UTL_GET_TIMING_MS(flex_timer)); continue; } auto const profile = to_profile(m, pedestrian_profile, elevation_costs); if (e == nullptr || profile != osr::search_profile::kWheelchair) { continue; // handled by get_offsets } utl::equal_ranges_linear( get_td_footpaths(*r.w_, *r.l_, *r.pl_, *r.tt_, rtt, *r.loc_tree_, *e, *r.matches_, n::location_idx_t::invalid(), pos, dir, profile, max, max_matching_distance, osr_params, *blocked), [](n::td_footpath const& a, n::td_footpath const& b) { return a.target_ == b.target_; }, [&](auto&& from, auto&& to) { ret.emplace(from->target_, utl::to_vec(from, to, [&](n::td_footpath const fp) { return n::routing::td_offset{ .valid_from_ = fp.valid_from_, .duration_ = fp.duration_, .transport_mode_id_ = static_cast(profile)}; })); }); } return ret; } n::routing::td_offsets_t routing::get_td_offsets( n::rt_timetable const* rtt, elevators const* e, place_t const& p, osr::direction const dir, std::vector const& modes, osr_parameters const& osr_params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, double const max_matching_distance, std::chrono::seconds const max, nigiri::routing::start_time_t const& start_time, stats_map_t& stats) const { return std::visit( utl::overloaded{ [&](tt_location l) { if (!osr_loaded(*this)) { return n::routing::td_offsets_t{}; } return ::motis::ep::get_td_offsets( *this, rtt, e, stop_to_osr_location(*this, l.l_), dir, modes, osr_params, pedestrian_profile, elevation_costs, max_matching_distance, max, start_time, stats); }, [&](osr::location const& pos) { return ::motis::ep::get_td_offsets( *this, rtt, e, pos, dir, modes, osr_params, pedestrian_profile, elevation_costs, max_matching_distance, max, start_time, stats); }}, p); } bool include_rental_provider( std::optional> const& rental_providers, std::optional> const& rental_provider_groups, gbfs::gbfs_provider const* provider) { if (provider == nullptr) { return false; } if ((!rental_providers || rental_providers->empty()) && (!rental_provider_groups || rental_provider_groups->empty())) { return true; } return (rental_provider_groups && utl::find(*rental_provider_groups, provider->group_id_) != end(*rental_provider_groups)) || (rental_providers && utl::find(*rental_providers, provider->id_) != end(*rental_providers)); } std::vector get_offsets( routing const& r, n::rt_timetable const* rtt, osr::location const& pos, osr::direction const dir, osr::elevation_storage const* elevations, std::vector const& modes, std::optional> const& form_factors, std::optional> const& propulsion_types, std::optional> const& rental_providers, std::optional> const& rental_provider_groups, bool const ignore_rental_return_constraints, osr_parameters const& osr_params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, std::chrono::seconds const max, double const max_matching_distance, gbfs::gbfs_routing_data& gbfs_rd, stats_map_t& stats) { if (!osr_loaded(r)) { return {}; } auto offsets = std::vector{}; auto ignore_walk = false; auto const handle_mode = [&](api::ModeEnum const m) { UTL_START_TIMING(timer); auto profile = to_profile(m, pedestrian_profile, elevation_costs); if (auto const rt = std::atomic_load(&r.rt_); rt->e_ && profile == osr::search_profile::kWheelchair) { return; // handled by get_td_offsets } if (osr::is_rental_profile(profile) && (!form_factors.has_value() || utl::any_of(*form_factors, [](auto const f) { return gbfs::get_osr_profile(gbfs::from_api_form_factor(f)) == osr::search_profile::kCarSharing; }))) { profile = osr::search_profile::kCarSharing; } auto const max_dist = get_max_distance(profile, max); auto const near_stops = get_stops_with_traffic(*r.tt_, rtt, *r.loc_tree_, pos, max_dist); auto const near_stop_locations = utl::to_vec( near_stops, [&](n::location_idx_t const l) { return stop_to_osr_location(r, l); }); auto const route = [&](osr::search_profile const p, osr::sharing_data const* sharing) { auto const params = to_profile_parameters(p, osr_params); auto const pos_match = r.l_->match(params, pos, false, dir, max_matching_distance, nullptr, p); auto const near_stop_matches = get_reverse_platform_way_matches( *r.l_, r.way_matches_, p, near_stops, near_stop_locations, dir, max_matching_distance); return osr::route(params, *r.w_, *r.l_, p, pos, near_stop_locations, pos_match, near_stop_matches, static_cast(max.count()), dir, nullptr, sharing, elevations); }; if (osr::is_rental_profile(profile)) { if (!gbfs_rd.has_data()) { return; } auto const max_dist_to_departure = dir == osr::direction::kForward ? get_max_distance(osr::search_profile::kFoot, max) : max_dist; auto providers = hash_set{}; gbfs_rd.data_->provider_rtree_.in_radius( pos.pos_, max_dist_to_departure, [&](auto const pi) { providers.insert(pi); }); for (auto const& pi : providers) { UTL_START_TIMING(provider_timer); auto const& provider = gbfs_rd.data_->providers_.at(pi); if (!include_rental_provider(rental_providers, rental_provider_groups, provider.get())) { continue; } auto provider_rd = std::shared_ptr{}; for (auto const& prod : provider->products_) { if ((prod.return_constraint_ == gbfs::return_constraint::kRoundtripStation && !ignore_rental_return_constraints) || !gbfs::products_match(prod, form_factors, propulsion_types)) { continue; } if (!provider_rd) { provider_rd = gbfs_rd.get_provider_routing_data(*provider); } auto const prod_ref = gbfs::gbfs_products_ref{pi, prod.idx_}; auto* prod_rd = gbfs_rd.get_products_routing_data(*provider, prod.idx_); auto const sharing = prod_rd->get_sharing_data( r.w_->n_nodes(), ignore_rental_return_constraints); auto const paths = route(gbfs::get_osr_profile(prod.form_factor_), &sharing); ignore_walk = true; for (auto const [p, l] : utl::zip(paths, near_stops)) { if (p.has_value()) { offsets.emplace_back(l, n::duration_t{static_cast( std::ceil(p->cost_ / 60.0))}, gbfs_rd.get_transport_mode(prod_ref)); } } } stats.emplace(fmt::format("prepare_{}_{}_{}", to_str(dir), fmt::streamed(m), provider->id_), UTL_GET_TIMING_MS(provider_timer)); } } else { auto const paths = route(profile, nullptr); for (auto const [p, l] : utl::zip(paths, near_stops)) { if (p.has_value()) { offsets.emplace_back( l, n::duration_t{static_cast(std::ceil(p->cost_ / 60.0))}, static_cast(profile)); } } } stats.emplace(fmt::format("prepare_{}_{}", to_str(dir), fmt::streamed(m)), UTL_GET_TIMING_MS(timer)); }; if (utl::find(modes, api::ModeEnum::RENTAL) != end(modes)) { handle_mode(api::ModeEnum::RENTAL); } for (auto const m : modes) { if (m == api::ModeEnum::RENTAL || m == api::ModeEnum::FLEX || (m == api::ModeEnum::WALK && ignore_walk)) { continue; } handle_mode(m); } return offsets; } n::interval shrink(bool const keep_late, std::size_t const max_size, n::interval search_interval, std::vector& journeys) { if (journeys.size() <= max_size) { return search_interval; } if (keep_late) { auto cutoff_it = std::next(journeys.rbegin(), static_cast(max_size - 1)); auto last_arr_time = cutoff_it->start_time_; ++cutoff_it; while (cutoff_it != rend(journeys) && cutoff_it->start_time_ == last_arr_time) { ++cutoff_it; } if (cutoff_it == rend(journeys)) { return search_interval; } search_interval.from_ = cutoff_it->start_time_ + std::chrono::minutes{1}; journeys.erase(begin(journeys), cutoff_it.base()); } else { auto cutoff_it = std::next(begin(journeys), static_cast(max_size - 1)); auto last_dep_time = cutoff_it->start_time_; while (cutoff_it != end(journeys) && cutoff_it->start_time_ == last_dep_time) { ++cutoff_it; } if (cutoff_it == end(journeys)) { return search_interval; } search_interval.to_ = cutoff_it->start_time_; journeys.erase(cutoff_it, end(journeys)); } return search_interval; } std::vector routing::get_offsets( n::rt_timetable const* rtt, place_t const& p, osr::direction const dir, std::vector const& modes, std::optional> const& form_factors, std::optional> const& propulsion_types, std::optional> const& rental_providers, std::optional> const& rental_provider_groups, bool const ignore_rental_return_constraints, osr_parameters const& osr_params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, std::chrono::seconds const max, double const max_matching_distance, gbfs::gbfs_routing_data& gbfs_rd, stats_map_t& stats) const { auto const do_get_offsets = [&](osr::location const pos) { return ::motis::ep::get_offsets( *this, rtt, pos, dir, elevations_, modes, form_factors, propulsion_types, rental_providers, rental_provider_groups, ignore_rental_return_constraints, osr_params, pedestrian_profile, elevation_costs, max, max_matching_distance, gbfs_rd, stats); }; return std::visit( utl::overloaded{ [&](tt_location const l) { if (!osr_loaded(*this)) { return station_start(l.l_); } auto offsets = do_get_offsets(stop_to_osr_location(*this, l.l_)); for_each_meta(*tt_, nigiri::routing::location_match_mode::kEquivalent, l.l_, [&](n::location_idx_t const c) { offsets.emplace_back(c, n::duration_t{0U}, 0U); }); return offsets; }, [&](osr::location const& pos) { return do_get_offsets(pos); }}, p); } std::pair> get_start_time( api::plan_params const& query, nigiri::timetable const* tt) { if (query.pageCursor_.has_value()) { return {cursor_to_query(*query.pageCursor_), std::nullopt}; } else { auto const t = std::chrono::time_point_cast( *query.time_.value_or(openapi::now())); utl::verify( tt == nullptr || tt->external_interval().contains(t), "query time {} is outside of loaded timetable window {}", t, tt ? tt->external_interval() : n::interval{}); auto const window = std::chrono::duration_cast(std::chrono::seconds{ query.searchWindow_ * (query.arriveBy_ ? -1 : 1)}); // TODO redundant minus return {{.start_time_ = query.timetableView_ && tt ? n::routing::start_time_t{n::interval{ tt->external_interval().clamp( query.arriveBy_ ? t - window : t), tt->external_interval().clamp( query.arriveBy_ ? t + n::duration_t{1} : t + window)}} : n::routing::start_time_t{t}, .extend_interval_earlier_ = query.arriveBy_, .extend_interval_later_ = !query.arriveBy_}, t}; } } std::pair, n::duration_t> routing::route_direct( elevators const* e, gbfs::gbfs_routing_data& gbfs_rd, n::lang_t const& lang, api::Place const& from, api::Place const& to, std::vector const& modes, std::optional> const& form_factors, std::optional> const& propulsion_types, std::optional> const& rental_providers, std::optional> const& rental_provider_groups, bool const ignore_rental_return_constraints, n::unixtime_t const time, bool const arrive_by, osr_parameters const& osr_params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, std::chrono::seconds max, double const max_matching_distance, double const fastest_direct_factor, bool const detailed_legs, unsigned const api_version) const { if (!w_ || !l_) { return {}; } auto fastest_direct = kInfinityDuration; auto cache = street_routing_cache_t{}; auto itineraries = std::vector{}; auto const route_with_profile = [&](output const& out) { auto itinerary = street_routing( *w_, *l_, e, elevations_, lang, from, to, out, arrive_by ? std::nullopt : std::optional{time}, arrive_by ? std::optional{time} : std::nullopt, max_matching_distance, osr_params, cache, *blocked, api_version, detailed_legs, max); if (itinerary.legs_.empty()) { return false; } auto const duration = std::chrono::duration_cast( std::chrono::seconds{itinerary.duration_}); if (duration < fastest_direct) { fastest_direct = duration; } itineraries.emplace_back(std::move(itinerary)); return true; }; for (auto const& m : modes) { if (m == api::ModeEnum::FLEX) { utl::verify(tt_ && tags_ && fa_, "FLEX requires timetable"); auto const routings = flex::get_flex_routings( *tt_, *loc_tree_, time, get_location(from).pos_, osr::direction::kForward, max); for (auto const& [_, ids] : routings) { route_with_profile(flex::flex_output{*w_, *l_, pl_, matches_, ae_, tz_, *tags_, *tt_, *fa_, ids.front()}); } } else if (m == api::ModeEnum::CAR || m == api::ModeEnum::BIKE || m == api::ModeEnum::CAR_PARKING || m == api::ModeEnum::CAR_DROPOFF || m == api::ModeEnum::DEBUG_BUS_ROUTE || m == api::ModeEnum::DEBUG_RAILWAY_ROUTE || m == api::ModeEnum::DEBUG_FERRY_ROUTE || m == api::ModeEnum::WALK) { route_with_profile(default_output{ *w_, to_profile(m, pedestrian_profile, elevation_costs)}); } else if (m == api::ModeEnum::RENTAL && gbfs_rd.has_data()) { // use foot because this is always forward search and we need to walk to // the station/vehicle auto const max_dist = get_max_distance(osr::search_profile::kFoot, max); auto providers = hash_set{}; auto routed = 0U; gbfs_rd.data_->provider_rtree_.in_radius( {from.lat_, from.lon_}, max_dist, [&](auto const pi) { providers.insert(pi); }); for (auto const& pi : providers) { auto const& provider = gbfs_rd.data_->providers_.at(pi); if (!include_rental_provider(rental_providers, rental_provider_groups, provider.get())) { continue; } for (auto const& prod : provider->products_) { if (!gbfs::products_match(prod, form_factors, propulsion_types)) { continue; } route_with_profile(gbfs::gbfs_output{ *w_, gbfs_rd, gbfs::gbfs_products_ref{provider->idx_, prod.idx_}, ignore_rental_return_constraints}); ++routed; } } // if we omitted the WALK routing but didn't have any rental providers // in the area, we need to do WALK routing now if (routed == 0U && utl::find(modes, api::ModeEnum::WALK) != end(modes)) { route_with_profile(default_output{ *w_, to_profile(api::ModeEnum::WALK, pedestrian_profile, elevation_costs)}); } } } utl::erase_duplicates(itineraries); return {itineraries, fastest_direct != kInfinityDuration ? std::chrono::round( fastest_direct * fastest_direct_factor) : fastest_direct}; } using stats_map_t = std::map; stats_map_t join(auto&&... maps) { auto ret = std::map{}; auto const add = [&](std::map const& x) { ret.insert(begin(x), end(x)); }; (add(maps), ...); return ret; } void remove_slower_than_fastest_direct(n::routing::query& q) { if (!q.fastest_direct_) { return; } constexpr auto const kMaxDuration = n::duration_t{std::numeric_limits::max()}; auto const worse_than_fastest_direct = [&](n::duration_t const min) { return [&, min](auto const& o) { return o.duration() < nigiri::footpath::kMaxDuration && o.duration() + min >= q.fastest_direct_; }; }; auto const get_min_duration = [&](auto&& x) { return x.empty() ? kMaxDuration : utl::min_element(x, [](auto&& a, auto&& b) { return a.duration() < b.duration(); })->duration(); }; auto min_start = get_min_duration(q.start_); for (auto const& [_, v] : q.td_start_) { min_start = std::min(min_start, get_min_duration(v)); } auto min_dest = get_min_duration(q.destination_); for (auto const& [_, v] : q.td_dest_) { min_dest = std::min(min_dest, get_min_duration(v)); } utl::erase_if(q.start_, worse_than_fastest_direct(min_dest)); utl::erase_if(q.destination_, worse_than_fastest_direct(min_start)); for (auto& [k, v] : q.td_start_) { utl::erase_if(v, worse_than_fastest_direct(min_dest)); } for (auto& [k, v] : q.td_dest_) { utl::erase_if(v, worse_than_fastest_direct(min_start)); } } std::vector get_via_stops( n::timetable const& tt, tag_lookup const& tags, std::optional> const& vias, std::vector const& times, bool const reverse) { if (!vias.has_value()) { return {}; } auto ret = std::vector{}; for (auto i = 0U; i != vias->size(); ++i) { ret.push_back({tags.get_location(tt, (*vias)[i]), n::duration_t{i < times.size() ? times[i] : 0}}); } if (reverse) { std::reverse(begin(ret), end(ret)); } return ret; } std::vector deduplicate(std::vector m) { utl::erase_duplicates(m); return m; }; api::plan_response routing::operator()(boost::urls::url_view const& url) const { metrics_->routing_requests_.Increment(); auto const query = api::plan_params{url.params()}; utl::verify( !query.maxItineraries_.has_value() || (*query.maxItineraries_ >= 1 && *query.maxItineraries_ >= query.numItineraries_), "maxItineraries={} < numItineraries={}", query.maxItineraries_.value_or(0), query.numItineraries_); auto const rt = std::atomic_load(&rt_); auto const rtt = rt->rtt_.get(); auto const e = rt->e_.get(); auto gbfs_rd = gbfs::gbfs_routing_data{w_, l_, gbfs_}; if (blocked.get() == nullptr && w_ != nullptr) { blocked.reset(new osr::bitvec{w_->n_nodes()}); } auto const api_version = get_api_version(url); auto const deduplicate = [](auto m) { utl::erase_duplicates(m); return m; }; auto const& lang = query.language_; auto const pre_transit_modes = deduplicate(query.preTransitModes_); auto const post_transit_modes = deduplicate(query.postTransitModes_); auto const direct_modes = deduplicate(query.directModes_); auto const from = get_place(tt_, tags_, query.fromPlace_); auto const to = get_place(tt_, tags_, query.toPlace_); auto from_p = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, from); auto to_p = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, to); if (from_p.vertexType_ == api::VertexTypeEnum::NORMAL) { from_p.name_ = "START"; } if (to_p.vertexType_ == api::VertexTypeEnum::NORMAL) { to_p.name_ = "END"; } auto const& start = query.arriveBy_ ? to : from; auto const& dest = query.arriveBy_ ? from : to; auto const& start_modes = query.arriveBy_ ? post_transit_modes : pre_transit_modes; auto const& dest_modes = query.arriveBy_ ? pre_transit_modes : post_transit_modes; auto const& start_form_factors = query.arriveBy_ ? query.postTransitRentalFormFactors_ : query.preTransitRentalFormFactors_; auto const& dest_form_factors = query.arriveBy_ ? query.preTransitRentalFormFactors_ : query.postTransitRentalFormFactors_; auto const& start_propulsion_types = query.arriveBy_ ? query.postTransitRentalPropulsionTypes_ : query.preTransitRentalPropulsionTypes_; auto const& dest_propulsion_types = query.arriveBy_ ? query.postTransitRentalPropulsionTypes_ : query.preTransitRentalPropulsionTypes_; auto const& start_rental_providers = query.arriveBy_ ? query.postTransitRentalProviders_ : query.preTransitRentalProviders_; auto const& dest_rental_providers = query.arriveBy_ ? query.preTransitRentalProviders_ : query.postTransitRentalProviders_; auto const& start_rental_provider_groups = query.arriveBy_ ? query.postTransitRentalProviderGroups_ : query.preTransitRentalProviderGroups_; auto const& dest_rental_provider_groups = query.arriveBy_ ? query.preTransitRentalProviderGroups_ : query.postTransitRentalProviderGroups_; auto const start_ignore_return_constraints = query.arriveBy_ ? query.ignorePostTransitRentalReturnConstraints_ : query.ignorePreTransitRentalReturnConstraints_; auto const dest_ignore_return_constraints = query.arriveBy_ ? query.ignorePreTransitRentalReturnConstraints_ : query.ignorePostTransitRentalReturnConstraints_; utl::verify( query.searchWindow_ / 60 < config_.get_limits().plan_max_search_window_minutes_, "maximum searchWindow size exceeded"); auto const max_transfers = query.maxTransfers_.has_value() && *query.maxTransfers_ <= n::routing::kMaxTransfers ? (*query.maxTransfers_ - (api_version < 3 ? 1 : 0)) : n::routing::kMaxTransfers; auto const osr_params = get_osr_parameters(query); auto const detailed_transfers = query.detailedTransfers_.value_or(query.detailedLegs_); auto const [start_time, t] = get_start_time(query, tt_); UTL_START_TIMING(direct); auto [direct, fastest_direct] = t.has_value() && !direct_modes.empty() && w_ && l_ ? route_direct( e, gbfs_rd, lang, from_p, to_p, direct_modes, query.directRentalFormFactors_, query.directRentalPropulsionTypes_, query.directRentalProviders_, query.directRentalProviderGroups_, query.ignoreDirectRentalReturnConstraints_, *t, query.arriveBy_, osr_params, query.pedestrianProfile_, query.elevationCosts_, std::min(std::chrono::seconds{query.maxDirectTime_}, std::chrono::seconds{ config_.get_limits() .street_routing_max_direct_seconds_}), query.maxMatchingDistance_, query.fastestDirectFactor_, query.detailedLegs_, api_version) : std::pair{std::vector{}, kInfinityDuration}; UTL_STOP_TIMING(direct); if (!query.transitModes_.empty() && fastest_direct > 5min && max_transfers >= 0) { utl::verify(tt_ != nullptr && tags_ != nullptr && loc_tree_ != nullptr, "mode=TRANSIT requires timetable to be loaded"); auto const max_results = config_.get_limits().plan_max_results_; utl::verify( query.numItineraries_ <= max_results, "maximum number of minimum itineraries is {}", max_results); auto const max_timeout = std::chrono::seconds{config_.get_limits().routing_max_timeout_seconds_}; utl::verify( !query.timeout_.has_value() || std::chrono::seconds{*query.timeout_} <= max_timeout, "maximum allowed timeout is {}", max_timeout); auto const with_odm_pre_transit = utl::find(pre_transit_modes, api::ModeEnum::ODM) != end(pre_transit_modes); auto const with_odm_post_transit = utl::find(post_transit_modes, api::ModeEnum::ODM) != end(post_transit_modes); auto const with_odm_direct = utl::find(direct_modes, api::ModeEnum::ODM) != end(direct_modes); auto const with_ride_sharing_pre_transit = utl::find(pre_transit_modes, api::ModeEnum::RIDE_SHARING) != end(pre_transit_modes); auto const with_ride_sharing_post_transit = utl::find(post_transit_modes, api::ModeEnum::RIDE_SHARING) != end(post_transit_modes); auto const with_ride_sharing_direct = utl::find(direct_modes, api::ModeEnum::RIDE_SHARING) != end(direct_modes); if (with_odm_pre_transit || with_odm_post_transit || with_odm_direct || with_ride_sharing_pre_transit || with_ride_sharing_post_transit || with_ride_sharing_direct) { utl::verify(config_.has_prima(), "PRIMA not configured"); return odm::meta_router{*this, query, pre_transit_modes, post_transit_modes, direct_modes, from, to, from_p, to_p, start_time, direct, fastest_direct, with_odm_pre_transit, with_odm_post_transit, with_odm_direct, with_ride_sharing_pre_transit, with_ride_sharing_post_transit, with_ride_sharing_direct, api_version} .run(); } auto const pre_transit_time = std::min( std::chrono::seconds{query.maxPreTransitTime_}, std::chrono::seconds{ config_.get_limits().street_routing_max_prepost_transit_seconds_}); auto const post_transit_time = std::min( std::chrono::seconds{query.maxPostTransitTime_}, std::chrono::seconds{ config_.get_limits().street_routing_max_prepost_transit_seconds_}); UTL_START_TIMING(query_preparation); auto prepare_stats = std::map{}; auto const use_radius_start = query.radius_.has_value() && std::holds_alternative(start); auto const use_radius_dest = query.radius_.has_value() && std::holds_alternative(dest); auto q = n::routing::query{ .start_time_ = start_time.start_time_, .start_match_mode_ = use_radius_start ? n::routing::location_match_mode::kIntermodal : get_match_mode(*this, start), .dest_match_mode_ = use_radius_dest ? n::routing::location_match_mode::kIntermodal : get_match_mode(*this, dest), .use_start_footpaths_ = !use_radius_start && !is_intermodal(*this, start), .start_ = use_radius_start ? radius_offsets(*loc_tree_, std::get(start).pos_, *query.radius_) : get_offsets( rtt, start, query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward, start_modes, start_form_factors, start_propulsion_types, start_rental_providers, start_rental_provider_groups, start_ignore_return_constraints, osr_params, query.pedestrianProfile_, query.elevationCosts_, query.arriveBy_ ? post_transit_time : pre_transit_time, query.maxMatchingDistance_, gbfs_rd, prepare_stats), .destination_ = use_radius_dest ? radius_offsets(*loc_tree_, std::get(dest).pos_, *query.radius_) : get_offsets( rtt, dest, query.arriveBy_ ? osr::direction::kForward : osr::direction::kBackward, dest_modes, dest_form_factors, dest_propulsion_types, dest_rental_providers, dest_rental_provider_groups, dest_ignore_return_constraints, osr_params, query.pedestrianProfile_, query.elevationCosts_, query.arriveBy_ ? pre_transit_time : post_transit_time, query.maxMatchingDistance_, gbfs_rd, prepare_stats), .td_start_ = get_td_offsets( rtt, e, start, query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward, start_modes, osr_params, query.pedestrianProfile_, query.elevationCosts_, query.maxMatchingDistance_, query.arriveBy_ ? post_transit_time : pre_transit_time, start_time.start_time_, prepare_stats), .td_dest_ = get_td_offsets( rtt, e, dest, query.arriveBy_ ? osr::direction::kForward : osr::direction::kBackward, dest_modes, osr_params, query.pedestrianProfile_, query.elevationCosts_, query.maxMatchingDistance_, query.arriveBy_ ? pre_transit_time : post_transit_time, start_time.start_time_, prepare_stats), .max_transfers_ = static_cast(max_transfers), .max_travel_time_ = query.maxTravelTime_ .and_then([](std::int64_t const dur) { return std::optional{n::duration_t{dur}}; }) .value_or(kInfinityDuration), .min_connection_count_ = static_cast(query.numItineraries_), .extend_interval_earlier_ = start_time.extend_interval_earlier_, .extend_interval_later_ = start_time.extend_interval_later_, .prf_idx_ = static_cast( query.useRoutedTransfers_ ? query.requireCarTransport_ ? n::kCarProfile : query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR ? n::kWheelchairProfile : n::kFootProfile : 0U), .allowed_claszes_ = to_clasz_mask(query.transitModes_), .require_bike_transport_ = query.requireBikeTransport_, .require_car_transport_ = query.requireCarTransport_, .transfer_time_settings_ = n::routing::transfer_time_settings{ .default_ = (query.minTransferTime_ == 0 && query.additionalTransferTime_ == 0 && query.transferTimeFactor_ == 1.0), .min_transfer_time_ = n::duration_t{query.minTransferTime_}, .additional_time_ = n::duration_t{query.additionalTransferTime_}, .factor_ = static_cast(query.transferTimeFactor_)}, .via_stops_ = get_via_stops(*tt_, *tags_, query.via_, query.viaMinimumStay_, query.arriveBy_), .fastest_direct_ = fastest_direct == kInfinityDuration ? std::nullopt : std::optional{fastest_direct}, .fastest_direct_factor_ = query.fastestDirectFactor_, .slow_direct_ = query.slowDirect_, .fastest_slow_direct_factor_ = query.fastestSlowDirectFactor_}; remove_slower_than_fastest_direct(q); UTL_STOP_TIMING(query_preparation); if (tt_->locations_.footpaths_out_.at(q.prf_idx_).empty()) { q.prf_idx_ = 0U; } auto const query_stats = stats_map_t{{"direct", UTL_TIMING_MS(direct)}, {"prepare", UTL_TIMING_MS(query_preparation)}, {"n_start_offsets", q.start_.size()}, {"n_dest_offsets", q.destination_.size()}, {"n_td_start_offsets", q.td_start_.size()}, {"n_td_dest_offsets", q.td_dest_.size()}}; auto r = n::routing::routing_result{}; auto algorithm = query.algorithm_; auto search_state = n::routing::search_state{}; while (true) { if (algorithm == api::algorithmEnum::PONG && query.timetableView_ && // arriveBy | extend_later | PONG applicable // ---------+---------------+--------------------- // FALSE | FALSE | FALSE => rRAPTOR // FALSE | TRUE | TRUE => PONG // TRUE | FALSE | TRUE => PONG // TRUE | TRUE | FALSE => rRAPTOR query.arriveBy_ != start_time.extend_interval_later_) { try { auto raptor_state = n::routing::raptor_state{}; r = n::routing::pong_search( *tt_, rtt, search_state, raptor_state, q, query.arriveBy_ ? n::direction::kBackward : n::direction::kForward, query.timeout_.has_value() ? std::chrono::seconds{*query.timeout_} : max_timeout); } catch (std::exception const& e) { std::cout << "PONG EXCEPTION: " << e.what() << "\n"; algorithm = api::algorithmEnum::RAPTOR; continue; } } else if (algorithm == api::algorithmEnum::RAPTOR || tbd_ == nullptr || (rtt != nullptr && rtt->n_rt_transports() != 0U) || query.arriveBy_ || q.prf_idx_ != tbd_->prf_idx_ || q.allowed_claszes_ != n::routing::all_clasz_allowed() || !q.td_start_.empty() || !q.td_dest_.empty() || !q.transfer_time_settings_.default_ || !q.via_stops_.empty() || q.require_bike_transport_ || q.require_car_transport_) { auto raptor_state = n::routing::raptor_state{}; r = n::routing::raptor_search( *tt_, rtt, search_state, raptor_state, q, query.arriveBy_ ? n::direction::kBackward : n::direction::kForward, query.timeout_.has_value() ? std::chrono::seconds{*query.timeout_} : max_timeout); } else { auto tb_state = n::routing::tb::query_state{*tt_, *tbd_}; r = n::routing::tb::tb_search(*tt_, search_state, tb_state, q); } break; } metrics_->routing_journeys_found_.Increment( static_cast(r.journeys_->size())); metrics_->routing_execution_duration_seconds_total_.Observe( static_cast(r.search_stats_.execute_time_.count()) / 1000.0); if (!r.journeys_->empty()) { metrics_->routing_journey_duration_seconds_.Observe(static_cast( to_seconds(r.journeys_->begin()->arrival_time() - r.journeys_->begin()->departure_time()))); } auto journeys = r.journeys_->els_; auto search_interval = r.interval_; if (query.maxItineraries_.has_value()) { search_interval = shrink(start_time.extend_interval_earlier_, static_cast(*query.maxItineraries_), r.interval_, journeys); } direct_filter(direct, journeys); return { .debugOutput_ = join(std::move(prepare_stats), std::move(query_stats), r.search_stats_.to_map(), std::move(r.algo_stats_)), .from_ = from_p, .to_ = to_p, .direct_ = std::move(direct), .itineraries_ = utl::to_vec( journeys, [&, cache = street_routing_cache_t{}](auto&& j) mutable { return journey_to_response( w_, l_, pl_, *tt_, *tags_, fa_, e, rtt, matches_, elevations_, shapes_, gbfs_rd, ae_, tz_, j, start, dest, cache, blocked.get(), query.requireCarTransport_ && query.useRoutedTransfers_, osr_params, query.pedestrianProfile_, query.elevationCosts_, query.joinInterlinedLegs_, detailed_transfers, query.detailedLegs_, query.withFares_, query.withScheduledSkippedStops_, config_.timetable_.value().max_matching_distance_, query.maxMatchingDistance_, api_version, query.ignorePreTransitRentalReturnConstraints_, query.ignorePostTransitRentalReturnConstraints_, query.language_); }), .previousPageCursor_ = fmt::format("EARLIER|{}", to_seconds(search_interval.from_)), .nextPageCursor_ = fmt::format("LATER|{}", to_seconds(search_interval.to_)), }; } return { .from_ = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, from), .to_ = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, to), .direct_ = std::move(direct), .itineraries_ = {}}; } } // namespace motis::ep ================================================ FILE: src/endpoints/stop_times.cc ================================================ #include "motis/endpoints/stop_times.h" #include #include #include "utl/concat.h" #include "utl/enumerate.h" #include "utl/erase_duplicates.h" #include "utl/verify.h" #include "net/bad_request_exception.h" #include "net/not_found_exception.h" #include "net/too_many_exception.h" #include "nigiri/routing/clasz_mask.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/rt/run.h" #include "nigiri/timetable.h" #include "nigiri/types.h" #include "motis/data.h" #include "motis/journey_to_response.h" #include "motis/parse_location.h" #include "motis/place.h" #include "motis/server.h" #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" #include "motis/timetable/modes_to_clasz_mask.h" #include "motis/timetable/time_conv.h" namespace n = nigiri; namespace motis::ep { struct ev_iterator { ev_iterator() = default; ev_iterator(ev_iterator const&) = delete; ev_iterator(ev_iterator&&) = delete; ev_iterator& operator=(ev_iterator const&) = delete; ev_iterator& operator=(ev_iterator&&) = delete; virtual ~ev_iterator() = default; virtual bool finished() const = 0; virtual n::unixtime_t time() const = 0; virtual n::rt::run get() const = 0; virtual void increment() = 0; }; struct static_ev_iterator : public ev_iterator { static_ev_iterator(n::timetable const& tt, n::rt_timetable const* rtt, n::route_idx_t const r, n::stop_idx_t const stop_idx, n::unixtime_t const start, n::event_type const ev_type, n::direction const dir) : tt_{tt}, rtt_{rtt}, day_{to_idx(tt_.day_idx_mam(start).first)}, end_day_{dir == n::direction::kForward ? to_idx(tt.day_idx(tt_.date_range_.to_)) : to_idx(tt.day_idx(tt_.date_range_.from_) - 1U)}, size_{tt_.route_transport_ranges_[r].size()}, i_{0}, r_{r}, stop_idx_{stop_idx}, ev_type_{ev_type}, dir_{dir} { seek_next(start); } ~static_ev_iterator() override = default; static_ev_iterator(static_ev_iterator const&) = delete; static_ev_iterator(static_ev_iterator&&) = delete; static_ev_iterator& operator=(static_ev_iterator const&) = delete; static_ev_iterator& operator=(static_ev_iterator&&) = delete; void seek_next(std::optional const start = std::nullopt) { while (!finished()) { for (; i_ < size_; ++i_) { if (start.has_value() && (dir_ == n::direction::kForward ? time() < *start : time() > *start)) { continue; } if (is_active()) { return; } } dir_ == n::direction::kForward ? ++day_ : --day_; i_ = 0; } } bool finished() const override { return day_ == end_day_; } n::unixtime_t time() const override { return tt_.event_time(t(), stop_idx_, ev_type_); } n::rt::run get() const override { assert(is_active()); return n::rt::run{ .t_ = t(), .stop_range_ = {stop_idx_, static_cast(stop_idx_ + 1U)}}; } void increment() override { ++i_; seek_next(); } private: bool is_active() const { auto const x = t(); auto const in_static = tt_.bitfields_[tt_.transport_traffic_days_[x.t_idx_]].test( to_idx(x.day_)); return rtt_ == nullptr ? in_static : in_static && rtt_->resolve_rt(x) == // only when no RT/cancelled n::rt_transport_idx_t::invalid(); } n::transport t() const { auto const idx = dir_ == n::direction::kForward ? i_ : size_ - i_ - 1; auto const t = tt_.route_transport_ranges_[r_][idx]; auto const day_offset = tt_.event_mam(r_, t, stop_idx_, ev_type_).days(); return n::transport{tt_.route_transport_ranges_[r_][idx], n::day_idx_t{to_idx(day_) - day_offset}}; } n::timetable const& tt_; n::rt_timetable const* rtt_; n::day_idx_t day_, end_day_; std::uint32_t size_; std::uint32_t i_; n::route_idx_t r_; n::stop_idx_t stop_idx_; n::event_type ev_type_; n::direction dir_; }; struct rt_ev_iterator : public ev_iterator { rt_ev_iterator(n::rt_timetable const& rtt, n::rt_transport_idx_t const rt_t, n::stop_idx_t const stop_idx, n::unixtime_t const start, n::event_type const ev_type, n::direction const dir, n::routing::clasz_mask_t const allowed_clasz) : rtt_{rtt}, stop_idx_{stop_idx}, rt_t_{rt_t}, ev_type_{ev_type}, finished_{ !n::routing::is_allowed( allowed_clasz, rtt.rt_transport_section_clasz_[rt_t].at(0)) || (dir == n::direction::kForward ? time() < start : time() > start)} { assert((ev_type == n::event_type::kDep && stop_idx_ < rtt_.rt_transport_location_seq_[rt_t].size() - 1U) || (ev_type == n::event_type::kArr && stop_idx_ > 0U)); } ~rt_ev_iterator() override = default; rt_ev_iterator(rt_ev_iterator const&) = delete; rt_ev_iterator(rt_ev_iterator&&) = delete; rt_ev_iterator& operator=(rt_ev_iterator const&) = delete; rt_ev_iterator& operator=(rt_ev_iterator&&) = delete; bool finished() const override { return finished_; } n::unixtime_t time() const override { return rtt_.unix_event_time(rt_t_, stop_idx_, ev_type_); } n::rt::run get() const override { return n::rt::run{ .stop_range_ = {stop_idx_, static_cast(stop_idx_ + 1U)}, .rt_ = rt_t_}; } void increment() override { finished_ = true; } n::rt_timetable const& rtt_; n::stop_idx_t stop_idx_; n::rt_transport_idx_t rt_t_; n::event_type ev_type_; bool finished_{false}; }; std::vector get_events( std::vector const& locations, n::timetable const& tt, n::rt_timetable const* rtt, n::unixtime_t const time, n::event_type const ev_type, n::direction const dir, std::size_t const min_count, std::size_t const max_count, n::routing::clasz_mask_t const allowed_clasz, bool const with_scheduled_skipped_stops, std::optional const max_time_diff) { auto iterators = std::vector>{}; if (rtt != nullptr) { for (auto const x : locations) { for (auto const rt_t : rtt->location_rt_transports_[x]) { auto const location_seq = rtt->rt_transport_location_seq_[rt_t]; for (auto const [stop_idx, s] : utl::enumerate(location_seq)) { if (n::stop{s}.location_idx() == x && ((ev_type == n::event_type::kDep && stop_idx != location_seq.size() - 1U) || (ev_type == n::event_type::kArr && stop_idx != 0U))) { if (!with_scheduled_skipped_stops) { auto const fr = n::rt::frun{ tt, rtt, n::rt::run{ .stop_range_ = {static_cast(stop_idx), static_cast(stop_idx + 1U)}, .rt_ = rt_t}}; auto const frs = fr[0]; if ((ev_type == n::event_type::kDep && !frs.get_scheduled_stop().in_allowed() && !frs.in_allowed()) || (ev_type == n::event_type::kArr && !frs.get_scheduled_stop().out_allowed() && !frs.out_allowed())) { continue; } } iterators.emplace_back(std::make_unique( *rtt, rt_t, static_cast(stop_idx), time, ev_type, dir, allowed_clasz)); } } } } } auto seen = n::hash_set>{}; for (auto const x : locations) { for (auto const r : tt.location_routes_[x]) { if (!n::routing::is_allowed(allowed_clasz, tt.route_clasz_[r])) { continue; } auto const location_seq = tt.route_location_seq_[r]; for (auto const [stop_idx, s] : utl::enumerate(location_seq)) { if (n::stop{s}.location_idx() == x && ((ev_type == n::event_type::kDep && stop_idx != location_seq.size() - 1U && (with_scheduled_skipped_stops || n::stop{s}.in_allowed())) || (ev_type == n::event_type::kArr && stop_idx != 0U && (with_scheduled_skipped_stops || n::stop{s}.out_allowed()))) && seen.emplace(r, static_cast(stop_idx)).second) { iterators.emplace_back(std::make_unique( tt, rtt, r, static_cast(stop_idx), time, ev_type, dir)); } } } } auto const all_finished = [&]() { return utl::all_of(iterators, [](auto const& it) { return it->finished(); }); }; auto const fwd = dir == n::direction::kForward; auto evs = std::vector{}; auto last_time = n::unixtime_t{}; while (!all_finished()) { auto const it = std::min_element( begin(iterators), end(iterators), [&](auto const& a, auto const& b) { if (a->finished() || b->finished()) { return a->finished() < b->finished(); } return fwd ? a->time() < b->time() : a->time() > b->time(); }); assert(!(*it)->finished()); auto const current_time = (*it)->time(); if ((!max_time_diff.has_value() || std::chrono::abs(current_time - time) > *max_time_diff) && (evs.size() >= min_count && current_time != last_time)) { break; } evs.emplace_back((*it)->get()); utl::verify( evs.size() <= max_count, "requesting for more than {} datapoints is not allowed", max_count); last_time = current_time; (*it)->increment(); } return evs; } std::vector other_stops_impl(n::rt::frun fr, n::event_type ev_type, n::timetable const* tt, tag_lookup const& tags, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const* ae, tz_map_t const* tz, n::lang_t const& lang) { auto const convert_stop = [&](n::rt::run_stop const& stop) { auto result = to_place(tt, &tags, w, pl, matches, ae, tz, lang, stop); if (ev_type == n::event_type::kDep || stop.fr_->stop_range_.from_ != stop.stop_idx_) { result.arrival_ = stop.time(n::event_type::kArr); result.scheduledArrival_ = stop.scheduled_time(n::event_type::kArr); } if (ev_type == n::event_type::kArr || stop.fr_->stop_range_.to_ - 1 != stop.stop_idx_) { result.departure_ = stop.time(n::event_type::kDep); result.scheduledDeparture_ = stop.scheduled_time(n::event_type::kDep); } return result; }; auto const orig_location = fr[fr.first_valid()].get_location_idx(); if (ev_type == nigiri::event_type::kDep) { ++fr.stop_range_.from_; fr.stop_range_.to_ = fr.size(); // Return next stops until one stop before the loop closes auto const it = utl::find_if(fr, [orig_location](n::rt::run_stop const& stop) { return orig_location == stop.get_location_idx(); }); auto result = utl::to_vec(fr.begin(), it, convert_stop); utl::verify(!result.empty(), "Departure is last stop in trip"); return result; } else { fr.stop_range_.from_ = 0; --fr.stop_range_.to_; // Return previous stops beginning one stop before the loop closes auto const it = std::find_if( fr.rbegin(), fr.rend(), [orig_location](n::rt::run_stop const& stop) { return orig_location == stop.get_location_idx(); }); auto result = utl::to_vec(it.base(), fr.end(), convert_stop); utl::verify(!result.empty(), "Arrival is first stop in trip"); return result; } } api::stoptimes_response stop_times::operator()( boost::urls::url_view const& url) const { auto const query = api::stoptimes_params{url.params()}; auto const& lang = query.language_; auto const api_version = get_api_version(url); auto const max_results = config_.get_limits().stoptimes_max_results_; utl::verify( query.n_.has_value() || query.window_.has_value(), "neither 'n' nor 'window' is provided"); if (query.n_.has_value()) { utl::verify(*query.n_ <= max_results, "n={} > {} not allowed", *query.n_, max_results); } utl::verify( query.stopId_.has_value() || (query.center_.has_value() && query.radius_.has_value()), "no stop and no center with radius (at least one is required)"); auto const query_stop = query.stopId_.and_then( [&](std::string const& x) { return tags_.find_location(tt_, x); }); auto const query_center = query.center_.and_then( [&](std::string const& x) { return parse_location(x); }); auto const center = query_stop .transform([&](n::location_idx_t const l) { return tt_.locations_.coordinates_[l]; }) .or_else([&]() { return query_center.transform( [](osr::location const& loc) { return loc.pos_; }); }); utl::verify( query_stop.has_value() || (query_center.has_value() && query.radius_.has_value()), "no radius: stop_found={}, center_parsed={}", query_stop.has_value(), query_center.has_value()); utl::verify( center.has_value(), "no coordinates: stop_found={}, center_parsed={}", query_stop.has_value(), query_center.has_value()); auto const allowed_clasz = to_clasz_mask(query.mode_); auto const [dir, time] = parse_cursor(query.pageCursor_.value_or(fmt::format( "{}|{}", query.direction_ .transform([](auto&& x) { return x == api::directionEnum::EARLIER ? "EARLIER" : "LATER"; }) .value_or(query.arriveBy_ ? "EARLIER" : "LATER"), std::chrono::duration_cast( query.time_.value_or(openapi::now())->time_since_epoch()) .count()))); auto locations = std::vector{}; auto const add = [&](n::location_idx_t const l) { if (query.exactRadius_) { locations.emplace_back(l); return; } auto const l_name = tt_.get_default_translation(tt_.locations_.names_[l]); utl::concat(locations, tt_.locations_.children_[l]); for (auto const& c : tt_.locations_.children_[l]) { utl::concat(locations, tt_.locations_.children_[c]); } for (auto const eq : tt_.locations_.equivalences_[l]) { if (tt_.get_default_translation(tt_.locations_.names_[eq]) == l_name) { locations.emplace_back(eq); utl::concat(locations, tt_.locations_.children_[eq]); for (auto const& c : tt_.locations_.children_[eq]) { utl::concat(locations, tt_.locations_.children_[c]); } } } }; if (query_stop.has_value()) { locations.emplace_back(tt_.locations_.get_root_idx(*query_stop)); if (query.radius_) { loc_rtree_.in_radius(*center, static_cast(*query.radius_), add); } else { add(*query_stop); } } else { loc_rtree_.in_radius(*center, static_cast(query.radius_.value()), add); } utl::erase_duplicates(locations); auto const rt = std::atomic_load(&rt_); auto const rtt = rt->rtt_.get(); auto const ev_type = query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep; auto const window = query.window_.transform([](auto const w) { return std::chrono::duration_cast(std::chrono::seconds{w}); }); auto events = get_events(locations, tt_, rtt, time, ev_type, dir, static_cast(query.n_.value_or(0)), static_cast(max_results), allowed_clasz, query.withScheduledSkippedStops_, window); auto const to_tuple = [&](n::rt::run const& x) { auto const fr_a = n::rt::frun{tt_, rtt, x}; return std::tuple{fr_a[0].time(ev_type), fr_a.is_scheduled() ? fr_a[0].get_trip_idx(ev_type) : n::trip_idx_t::invalid()}; }; utl::sort(events, [&](n::rt::run const& a, n::rt::run const& b) { return to_tuple(a) < to_tuple(b); }); events.erase(std::unique(begin(events), end(events), [&](n::rt::run const& a, n::rt::run const& b) { return to_tuple(a) == to_tuple(b); }), end(events)); return { .stopTimes_ = utl::to_vec( events, [&](n::rt::run const r) -> api::StopTime { auto const fr = n::rt::frun{tt_, rtt, r}; auto const s = fr[0]; auto const& agency = s.get_provider(ev_type); auto const run_cancelled = fr.is_cancelled(); auto place = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, query.language_, s); if (query.withAlerts_) { place.alerts_ = get_alerts(fr, std::pair{s, fr.stop_range_.from_ != 0U ? n::event_type::kArr : n::event_type::kDep}, true, query.language_); } if (fr.stop_range_.from_ != 0U) { place.arrival_ = {s.time(n::event_type::kArr)}; place.scheduledArrival_ = {s.scheduled_time(n::event_type::kArr)}; } if (fr.stop_range_.from_ != fr.size() - 1U) { place.departure_ = {s.time(n::event_type::kDep)}; place.scheduledDeparture_ = { s.scheduled_time(n::event_type::kDep)}; } auto const in_out_allowed = !run_cancelled && (ev_type == n::event_type::kArr ? s.out_allowed() : s.in_allowed()); auto const stop_cancelled = run_cancelled || (ev_type == n::event_type::kArr ? !s.out_allowed() && s.get_scheduled_stop().out_allowed() : !s.in_allowed() && s.get_scheduled_stop().in_allowed()); auto const trip_id = tags_.id(tt_, s, ev_type); auto const other_stops = [&](n::event_type desired_event) -> std::optional> { if (desired_event != ev_type || !query.fetchStops_.value_or(false)) { return std::nullopt; } return other_stops_impl(fr, ev_type, &tt_, tags_, w_, pl_, matches_, ae_, tz_, query.language_); }; return { .place_ = std::move(place), .mode_ = to_mode(s.get_clasz(ev_type), api_version), .realTime_ = r.is_rt(), .headsign_ = std::string{s.direction(lang, ev_type)}, .tripFrom_ = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, lang, s.get_first_trip_stop(ev_type)), .tripTo_ = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, lang, s.get_last_trip_stop(ev_type)), .agencyId_ = std::string{tt_.strings_.try_get(agency.id_).value_or("?")}, .agencyName_ = std::string{tt_.translate(lang, agency.name_)}, .agencyUrl_ = std::string{tt_.translate(lang, agency.url_)}, .routeId_ = tags_.route_id(s, ev_type), .routeUrl_ = std::string{s.route_url(ev_type, lang)}, .directionId_ = s.get_direction_id(ev_type) == 0 ? "0" : "1", .routeColor_ = to_str(s.get_route_color(ev_type).color_), .routeTextColor_ = to_str(s.get_route_color(ev_type).text_color_), .tripId_ = trip_id, .routeType_ = s.route_type(ev_type).and_then([](n::route_type_t const x) { return std::optional{to_idx(x)}; }), .routeShortName_ = std::string{api_version < 4 ? s.display_name(ev_type, lang) : s.route_short_name(ev_type, lang)}, .routeLongName_ = std::string{s.route_long_name(ev_type, lang)}, .tripShortName_ = std::string{s.trip_short_name(ev_type, lang)}, .displayName_ = std::string{s.display_name(ev_type, lang)}, .previousStops_ = other_stops(nigiri::event_type::kArr), .nextStops_ = other_stops(nigiri::event_type::kDep), .pickupDropoffType_ = in_out_allowed ? api::PickupDropoffTypeEnum::NORMAL : api::PickupDropoffTypeEnum::NOT_ALLOWED, .cancelled_ = stop_cancelled, .tripCancelled_ = run_cancelled, .source_ = fmt::format("{}", fmt::streamed(fr.dbg()))}; }), .place_ = query_stop .transform([&](n::location_idx_t const l) { return to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, lang, tt_location{l}); }) .or_else([&]() { return query_center.transform([](osr::location const& loc) { return to_place(loc, "center", std::nullopt); }); }) .value(), .previousPageCursor_ = events.empty() ? "" : fmt::format( "EARLIER|{}", to_seconds( n::rt::frun{tt_, rtt, events.front()}[0].time(ev_type) - std::chrono::minutes{1})), .nextPageCursor_ = events.empty() ? "" : fmt::format( "LATER|{}", to_seconds( n::rt::frun{tt_, rtt, events.back()}[0].time(ev_type) + std::chrono::minutes{1}))}; } } // namespace motis::ep ================================================ FILE: src/endpoints/tiles.cc ================================================ #include "motis/endpoints/tiles.h" #include #include "net/web_server/url_decode.h" #include "tiles/get_tile.h" #include "tiles/parse_tile_url.h" #include "tiles/perf_counter.h" #include "motis/tiles_data.h" #include "pbf_sdf_fonts_res.h" using namespace std::string_view_literals; namespace motis::ep { net::reply tiles::operator()(net::route_request const& req, bool) const { auto const url = boost::url_view{req.target()}; if (url.path().starts_with("/tiles/glyphs")) { std::string decoded; net::url_decode(url.path(), decoded); // Rewrite old font name "Noto Sans Display Regular" to "Noto Sans Regular". constexpr auto kDisplay = " Display"sv; auto res_name = decoded.substr(14); if (auto const display_pos = res_name.find(kDisplay); display_pos != std::string::npos) { res_name.erase(display_pos, kDisplay.length()); } try { auto const mem = pbf_sdf_fonts_res::get_resource(res_name); auto res = net::web_server::string_res_t{boost::beast::http::status::ok, req.version()}; res.body() = std::string_view{reinterpret_cast(mem.ptr_), mem.size_}; res.insert(boost::beast::http::field::content_type, "application/x-protobuf"); res.keep_alive(req.keep_alive()); return res; } catch (std::out_of_range const&) { throw net::not_found_exception{res_name}; } } auto const tile = ::tiles::parse_tile_url(url.path()); if (!tile.has_value()) { return net::web_server::empty_res_t{boost::beast::http::status::not_found, req.version()}; } auto pc = ::tiles::null_perf_counter{}; auto const rendered_tile = ::tiles::get_tile(tiles_data_.db_handle_, tiles_data_.pack_handle_, tiles_data_.render_ctx_, *tile, pc); auto res = net::web_server::string_res_t{boost::beast::http::status::ok, req.version()}; res.insert(boost::beast::http::field::content_type, "application/vnd.mapbox-vector-tile"); res.insert(boost::beast::http::field::content_encoding, "deflate"); res.body() = rendered_tile.value_or(""); res.keep_alive(req.keep_alive()); return res; } } // namespace motis::ep ================================================ FILE: src/endpoints/transfers.cc ================================================ #include "motis/endpoints/transfers.h" #include "osr/geojson.h" #include "osr/routing/route.h" #include "utl/pipes/all.h" #include "utl/pipes/transform.h" #include "utl/pipes/vec.h" #include "motis/constants.h" #include "motis/elevators/elevators.h" #include "motis/elevators/match_elevator.h" #include "motis/get_loc.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" #include "motis/tag_lookup.h" namespace json = boost::json; namespace n = nigiri; namespace motis::ep { api::transfers_response transfers::operator()( boost::urls::url_view const& url) const { auto const q = motis::api::transfers_params{url.params()}; auto const rt = std::atomic_load(&rt_); auto const e = rt->e_.get(); auto const l = tags_.get_location(tt_, q.id_); auto const neighbors = loc_rtree_.in_radius(tt_.locations_.coordinates_[l], kMaxDistance); auto footpaths = hash_map{}; for (auto const fp : tt_.locations_.footpaths_out_[0].at(l)) { footpaths[fp.target()].default_ = fp.duration().count(); } if (!tt_.locations_.footpaths_out_[n::kFootProfile].empty()) { for (auto const fp : tt_.locations_.footpaths_out_[n::kFootProfile].at(l)) { footpaths[fp.target()].foot_ = fp.duration().count(); } } if (!tt_.locations_.footpaths_out_[n::kWheelchairProfile].empty()) { for (auto const fp : tt_.locations_.footpaths_out_[n::kWheelchairProfile].at(l)) { footpaths[fp.target()].wheelchair_ = fp.duration().count(); } } if (!tt_.locations_.footpaths_out_[n::kCarProfile].empty()) { for (auto const fp : tt_.locations_.footpaths_out_[n::kCarProfile].at(l)) { footpaths[fp.target()].car_ = fp.duration().count(); } } auto const loc = get_loc(tt_, w_, pl_, matches_, l); for (auto const mode : {osr::search_profile::kFoot, osr::search_profile::kWheelchair}) { auto const results = osr::route( to_profile_parameters(mode, {}), w_, l_, mode, loc, utl::to_vec( neighbors, [&](auto&& l) { return get_loc(tt_, w_, pl_, matches_, l); }), c_.timetable_.value().max_footpath_length_ * 60U, osr::direction::kForward, c_.timetable_.value().max_matching_distance_, e == nullptr ? nullptr : &e->blocked_, nullptr, nullptr, [](osr::path const& p) { return p.uses_elevator_; }); for (auto const [n, r] : utl::zip(neighbors, results)) { if (r.has_value()) { auto& fp = footpaths[n]; auto const duration = std::ceil(r->cost_ / 60U); if (duration < n::footpath::kMaxDuration.count()) { switch (mode) { case osr::search_profile::kFoot: fp.footRouted_ = duration; break; case osr::search_profile::kWheelchair: fp.wheelchairRouted_ = duration; fp.wheelchairUsesElevator_ = r->uses_elevator_; break; default: std::unreachable(); } } } } } auto const to_place = [&](n::location_idx_t const l) -> api::Place { return { .name_ = std::string{tt_.get_default_translation(tt_.locations_.names_[l])}, .stopId_ = std::string{tt_.locations_.ids_[l].view()}, .lat_ = tt_.locations_.coordinates_[l].lat(), .lon_ = tt_.locations_.coordinates_[l].lng(), .level_ = pl_.get_level(w_, matches_[l]).to_float(), .vertexType_ = api::VertexTypeEnum::NORMAL}; }; return {.place_ = to_place(l), .root_ = to_place(tt_.locations_.get_root_idx(l)), .equivalences_ = utl::to_vec( tt_.locations_.equivalences_[l], [&](n::location_idx_t const eq) { return to_place(eq); }), .hasFootTransfers_ = !tt_.locations_.footpaths_out_[n::kFootProfile].empty(), .hasWheelchairTransfers_ = !tt_.locations_.footpaths_out_[n::kWheelchairProfile].empty(), .hasCarTransfers_ = !tt_.locations_.footpaths_out_[n::kCarProfile].empty(), .transfers_ = utl::to_vec(footpaths, [&](auto&& e) { e.second.to_ = to_place(e.first); return e.second; })}; } } // namespace motis::ep ================================================ FILE: src/endpoints/trip.cc ================================================ #include "motis/endpoints/trip.h" #include #include "net/not_found_exception.h" #include "nigiri/routing/journey.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/gtfsrt_resolve_run.h" #include "nigiri/timetable.h" #include "motis/constants.h" #include "motis/data.h" #include "motis/gbfs/routing_data.h" #include "motis/journey_to_response.h" #include "motis/parse_location.h" #include "motis/server.h" #include "motis/tag_lookup.h" namespace n = nigiri; namespace motis::ep { api::Itinerary trip::operator()(boost::urls::url_view const& url) const { auto const rt = std::atomic_load(&rt_); auto const rtt = rt->rtt_.get(); auto query = api::trip_params{url.params()}; auto const api_version = get_api_version(url); auto const [r, _] = tags_.get_trip(tt_, rtt, query.tripId_); utl::verify(r.valid(), "trip not found: tripId={}, tt={}", query.tripId_, tt_.external_interval()); auto fr = n::rt::frun{tt_, rtt, r}; fr.stop_range_.to_ = fr.size(); fr.stop_range_.from_ = 0U; auto const from_l = fr[0]; auto const to_l = fr[fr.size() - 1U]; auto const start_time = from_l.time(n::event_type::kDep); auto const dest_time = to_l.time(n::event_type::kArr); auto cache = street_routing_cache_t{}; auto blocked = osr::bitvec{}; auto gbfs_rd = gbfs::gbfs_routing_data{}; return journey_to_response( w_, l_, pl_, tt_, tags_, nullptr, nullptr, rtt, matches_, nullptr, shapes_, gbfs_rd, ae_, tz_, {.legs_ = {n::routing::journey::leg{ n::direction::kForward, from_l.get_location_idx(), to_l.get_location_idx(), start_time, dest_time, n::routing::journey::run_enter_exit{ fr, // NOLINT(cppcoreguidelines-slicing) fr.stop_range_.from_, static_cast(fr.stop_range_.to_ - 1U)}}}, .start_time_ = start_time, .dest_time_ = dest_time, .dest_ = to_l.get_location_idx(), .transfers_ = 0U}, tt_location{from_l.get_location_idx(), from_l.get_scheduled_location_idx()}, tt_location{to_l.get_location_idx()}, cache, &blocked, false, osr_parameters{}, api::PedestrianProfileEnum::FOOT, api::ElevationCostsEnum::NONE, query.joinInterlinedLegs_, true, query.detailedLegs_, false, query.withScheduledSkippedStops_, config_.timetable_.value().max_matching_distance_, kMaxMatchingDistance, api_version, false, false, query.language_); } } // namespace motis::ep ================================================ FILE: src/endpoints/update_elevator.cc ================================================ #include "motis/endpoints/update_elevator.h" #include "net/not_found_exception.h" #include "nigiri/rt/create_rt_timetable.h" #include "motis/constants.h" #include "motis/elevators/elevators.h" #include "motis/elevators/parse_fasta.h" #include "motis/get_loc.h" #include "motis/railviz.h" #include "motis/update_rtt_td_footpaths.h" namespace json = boost::json; namespace n = nigiri; namespace motis::ep { json::value update_elevator::operator()(json::value const& query) const { auto const& q = query.as_object(); auto const id = q.at("id").to_number(); auto const new_status = q.at("status").as_string() != "INACTIVE"; auto const new_out_of_service = parse_out_of_service(q); auto const rt_copy = std::atomic_load(&rt_); auto const e = rt_copy->e_.get(); utl::verify(e != nullptr, "elevators not available"); auto const rtt = rt_copy->rtt_.get(); auto elevators_copy = e->elevators_; auto const it = utl::find_if(elevators_copy, [&](auto&& x) { return x.id_ == id; }); utl::verify(it != end(elevators_copy), "id {} not found", id); it->status_ = new_status; it->out_of_service_ = new_out_of_service; it->state_changes_ = intervals_to_state_changes(it->out_of_service_, it->status_); auto tasks = hash_set>{}; loc_rtree_.in_radius(it->pos_, kElevatorUpdateRadius, [&](n::location_idx_t const l) { tasks.emplace(l, osr::direction::kForward); tasks.emplace(l, osr::direction::kBackward); }); auto new_e = elevators{w_, elevator_ids_, elevator_nodes_, std::move(elevators_copy)}; auto new_rtt = n::rt::create_rt_timetable(tt_, rtt->base_day_); update_rtt_td_footpaths( w_, l_, pl_, tt_, loc_rtree_, new_e, matches_, tasks, rtt, new_rtt, std::chrono::seconds{c_.timetable_.value().max_footpath_length_ * 60}); auto new_rt = std::make_shared( std::make_unique(std::move(new_rtt)), std::make_unique(std::move(new_e)), std::move(rt_copy->railviz_rt_)); std::atomic_store(&rt_, std::move(new_rt)); return json::string{{"success", true}}; } } // namespace motis::ep ================================================ FILE: src/flex/flex.cc ================================================ #include "motis/flex/flex.h" #include #include "utl/concat.h" #include "osr/lookup.h" #include "osr/routing/parameters.h" #include "osr/routing/profiles/foot.h" #include "osr/routing/route.h" #include "osr/ways.h" #include "motis/constants.h" #include "motis/data.h" #include "motis/endpoints/routing.h" #include "motis/flex/flex_areas.h" #include "motis/flex/flex_routing_data.h" #include "motis/match_platforms.h" #include "motis/osr/max_distance.h" namespace n = nigiri; namespace motis::flex { osr::sharing_data prepare_sharing_data(n::timetable const& tt, osr::ways const& w, osr::lookup const& lookup, osr::platforms const* pl, flex_areas const& fa, platform_matches_t const* pl_matches, mode_id const id, osr::direction const dir, flex_routing_data& frd) { auto const stop_seq = tt.flex_stop_seq_[tt.flex_transport_stop_seq_[id.get_flex_transport()]]; auto const from_stop = stop_seq.at(id.get_stop()); auto to_stops = std::vector{}; for (auto i = static_cast(id.get_stop()) + (dir == osr::direction::kForward ? 1 : -1); dir == osr::direction::kForward ? i < static_cast(stop_seq.size()) : i >= 0; dir == osr::direction::kForward ? ++i : --i) { to_stops.emplace_back(stop_seq.at(static_cast(i))); } // Count additional nodes and allocate bit vectors. auto n_nodes = w.n_nodes(); from_stop.apply(utl::overloaded{[&](n::location_group_idx_t const from_lg) { n_nodes += tt.location_group_locations_[from_lg].size(); }}); for (auto const& to_stop : to_stops) { to_stop.apply(utl::overloaded{[&](n::location_group_idx_t const to_lg) { n_nodes += tt.location_group_locations_[to_lg].size(); }}); } frd.additional_node_offset_ = w.n_nodes(); frd.additional_node_coordinates_.clear(); frd.additional_edges_.clear(); frd.start_allowed_.resize(n_nodes); frd.end_allowed_.resize(n_nodes); frd.through_allowed_.resize(n_nodes); frd.start_allowed_.zero_out(); frd.end_allowed_.zero_out(); frd.through_allowed_.one_out(); // Creates an additional node for the given timetable location // and adds additional edges to/from this node. auto next_add_node_idx = osr::node_idx_t{w.n_nodes()}; auto const add_tt_location = [&](n::location_idx_t const l) { frd.additional_nodes_.emplace_back(l); frd.additional_node_coordinates_.emplace_back( tt.locations_.coordinates_[l]); auto const pos = get_location(&tt, &w, pl, pl_matches, tt_location{l}); auto const l_additional_node_idx = next_add_node_idx++; auto const matches = lookup.match>( osr::foot::parameters{}, pos, false, osr::direction::kForward, kMaxGbfsMatchingDistance, nullptr); for (auto const& m : matches) { auto const handle_node = [&](osr::node_candidate const& node) { if (!node.valid() || node.dist_to_node_ > kMaxGbfsMatchingDistance) { return; } auto const edge_to_an = osr::additional_edge{ l_additional_node_idx, static_cast(node.dist_to_node_)}; auto& node_edges = frd.additional_edges_[node.node_]; if (utl::find(node_edges, edge_to_an) == end(node_edges)) { node_edges.emplace_back(edge_to_an); } auto& add_node_out = frd.additional_edges_[l_additional_node_idx]; auto const edge_from_an = osr::additional_edge{ node.node_, static_cast(node.dist_to_node_)}; if (utl::find(add_node_out, edge_from_an) == end(add_node_out)) { add_node_out.emplace_back(edge_from_an); } }; handle_node(m.left_); handle_node(m.right_); } return l_additional_node_idx; }; // Set start allowed in start area / location group. auto tmp = osr::bitvec{}; from_stop.apply(utl::overloaded{ [&](n::location_group_idx_t const from_lg) { for (auto const& l : tt.location_group_locations_[from_lg]) { frd.start_allowed_.set(add_tt_location(l), true); } }, [&](n::flex_area_idx_t const from_area) { fa.add_area(from_area, frd.start_allowed_, tmp); }}); // Set end allowed in follow-up areas / location groups. for (auto const& to_stop : to_stops) { to_stop.apply(utl::overloaded{ [&](n::location_group_idx_t const to_lg) { for (auto const& l : tt.location_group_locations_[to_lg]) { frd.end_allowed_.set(add_tt_location(l), true); } }, [&](n::flex_area_idx_t const to_area) { fa.add_area(to_area, frd.end_allowed_, tmp); }}); } return frd.to_sharing_data(); } n::interval get_relevant_days( n::timetable const& tt, n::routing::start_time_t const start_time) { auto const to_sys_days = [](n::unixtime_t const t) { return std::chrono::time_point_cast(t); }; auto const iv = std::visit( utl::overloaded{[&](n::unixtime_t const t) { return n::interval{to_sys_days(t) - date::days{2}, to_sys_days(t) + date::days{3}}; }, [&](n::interval const x) { return n::interval{to_sys_days(x.from_) - date::days{2}, to_sys_days(x.to_) + date::days{3}}; }}, start_time); return n::interval{tt.day_idx(iv.from_), tt.day_idx(iv.to_)}; } flex_routings_t get_flex_routings( n::timetable const& tt, point_rtree const& loc_rtree, n::routing::start_time_t const start_time, geo::latlng const& pos, osr::direction const dir, std::chrono::seconds const max) { auto routings = flex_routings_t{}; // Traffic days helpers. auto const day_idx_iv = get_relevant_days(tt, start_time); auto const is_active = [&](n::flex_transport_idx_t const t) { auto const& bitfield = tt.bitfields_[tt.flex_transport_traffic_days_[t]]; return utl::any_of(day_idx_iv, [&](n::day_idx_t const i) { return bitfield.test(to_idx(i)); }); }; // Stop index helper. auto const get_stop_idx = [&](n::flex_stop_seq_idx_t const stop_seq_idx, n::flex_stop_t const x) -> std::optional { auto const stops = tt.flex_stop_seq_[stop_seq_idx]; auto const is_last = [&](n::stop_idx_t const stop_idx) { return (dir == osr::direction::kBackward && stop_idx == 0U) || (dir == osr::direction::kForward && stop_idx == stops.size() - 1U); }; for (auto c = 0U; c != stops.size(); ++c) { auto const stop_idx = static_cast( dir == osr::direction::kForward ? c : stops.size() - c - 1); if (stops[stop_idx] == x && !is_last(stop_idx)) { return stop_idx; } } return std::nullopt; }; // Collect area transports. auto const add_area_flex_transports = [&](n::flex_area_idx_t const a) { for (auto const t : tt.flex_area_transports_[a]) { if (!is_active(t)) { continue; } auto const stop_idx = get_stop_idx(tt.flex_transport_stop_seq_[t], a); if (stop_idx.has_value()) { routings[std::pair{tt.flex_transport_stop_seq_[t], *stop_idx}] .emplace_back(t, *stop_idx, dir); } } }; auto const box = geo::box{pos, get_max_distance(osr::search_profile::kFoot, max)}; tt.flex_area_rtree_.search(box.min_.lnglat_float(), box.max_.lnglat_float(), [&](auto&&, auto&&, n::flex_area_idx_t const a) { add_area_flex_transports(a); return true; }); // Collect location group transports. auto location_groups = hash_set{}; loc_rtree.in_radius(pos, get_max_distance(osr::search_profile::kFoot, max), [&](n::location_idx_t const l) { for (auto const lg : tt.location_location_groups_[l]) { location_groups.emplace(lg); } return true; }); for (auto const& lg : location_groups) { for (auto const t : tt.location_group_transports_[lg]) { if (!is_active(t)) { continue; } auto const stop_idx = get_stop_idx(tt.flex_transport_stop_seq_[t], lg); if (stop_idx.has_value()) { routings[std::pair{tt.flex_transport_stop_seq_[t], *stop_idx}] .emplace_back(t, *stop_idx, dir); } } } return routings; } bool is_in_flex_stop(n::timetable const& tt, osr::ways const& w, flex_areas const& fa, flex_routing_data const& frd, n::flex_stop_t const& s, osr::node_idx_t const n) { return s.apply(utl::overloaded{ [&](n::flex_area_idx_t const a) { return !w.is_additional_node(n) && n != osr::node_idx_t::invalid() && fa.is_in_area(a, w.get_node_pos(n)); }, [&](n::location_group_idx_t const lg) { if (!w.is_additional_node(n)) { return false; } auto const locations = tt.location_group_locations_.at(lg); auto const l = frd.get_additional_node(n); return utl::find(locations, l) != end(locations); }}); } void add_flex_td_offsets(osr::ways const& w, osr::lookup const& lookup, osr::platforms const* pl, platform_matches_t const* matches, way_matches_storage const* way_matches, n::timetable const& tt, flex_areas const& fa, point_rtree const& loc_rtree, n::routing::start_time_t const start_time, osr::location const& pos, osr::direction const dir, std::chrono::seconds const max, double const max_matching_distance, osr_parameters const& osr_params, flex_routing_data& frd, n::routing::td_offsets_t& ret, std::map& stats) { UTL_START_TIMING(flex_lookup_timer); auto const max_dist = get_max_distance(osr::search_profile::kCarSharing, max); auto const near_stops = loc_rtree.in_radius(pos.pos_, max_dist); auto const near_stop_locations = utl::to_vec(near_stops, [&](n::location_idx_t const l) { return get_location(&tt, &w, pl, matches, tt_location{l}); }); auto const params = to_profile_parameters(osr::search_profile::kCarSharing, osr_params); auto const pos_match = lookup.match(params, pos, false, dir, max_matching_distance, nullptr, osr::search_profile::kCarSharing); auto const near_stop_matches = get_reverse_platform_way_matches( lookup, way_matches, osr::search_profile::kCarSharing, near_stops, near_stop_locations, dir, max_matching_distance); auto const routings = get_flex_routings(tt, loc_rtree, start_time, pos.pos_, dir, max); stats.emplace(fmt::format("prepare_{}_FLEX_lookup", to_str(dir)), UTL_GET_TIMING_MS(flex_lookup_timer)); for (auto const& [stop_seq, transports] : routings) { UTL_START_TIMING(routing_timer); auto const sharing_data = prepare_sharing_data( tt, w, lookup, pl, fa, matches, transports.front(), dir, frd); auto const paths = osr::route(params, w, lookup, osr::search_profile::kCarSharing, pos, near_stop_locations, pos_match, near_stop_matches, static_cast(max.count()), dir, nullptr, &sharing_data, nullptr); auto const day_idx_iv = get_relevant_days(tt, start_time); for (auto const id : transports) { auto const t = id.get_flex_transport(); auto const from_stop_idx = id.get_stop(); for (auto const day_idx : day_idx_iv) { if (!tt.bitfields_[tt.flex_transport_traffic_days_[t]].test( to_idx(day_idx))) { continue; } auto const day = tt.internal_interval().from_ + to_idx(day_idx) * date::days{1U}; auto const from_stop_time_window = tt.flex_transport_stop_time_windows_[t][from_stop_idx]; auto const abs_from_stop_iv = n::interval{ day + from_stop_time_window.from_, day + from_stop_time_window.to_}; for (auto const [p, s, l] : utl::zip(paths, near_stop_locations, near_stops)) { if (p.has_value()) { auto const rel_to_stop_idx = 0U; auto const to_stop_idx = static_cast( dir == osr::direction::kForward ? from_stop_idx + rel_to_stop_idx : from_stop_idx - rel_to_stop_idx); auto const duration = n::duration_t{p->cost_ / 60}; auto const to_stop_time_window = tt.flex_transport_stop_time_windows_[t][to_stop_idx]; auto const abs_to_stop_iv = n::interval{ day + to_stop_time_window.from_, day + to_stop_time_window.to_}; auto const iv_at_to_stop = (dir == osr::direction::kForward ? abs_from_stop_iv >> duration : abs_from_stop_iv << duration) .intersect(abs_to_stop_iv); auto const iv_at_from_stop = dir == osr::direction::kForward ? iv_at_to_stop << duration : iv_at_to_stop >> duration; auto& offsets = ret[l]; if (offsets.empty()) { offsets.emplace_back(n::unixtime_t{n::i32_minutes{0U}}, n::footpath::kMaxDuration, id.to_id()); } offsets.emplace_back(iv_at_from_stop.from_, duration, id.to_id()); offsets.emplace_back(iv_at_from_stop.to_, n::footpath::kMaxDuration, id.to_id()); } } } } stats.emplace( fmt::format("prepare_{}_FLEX_{}", to_str(dir), tt.flex_stop_seq_[stop_seq.first][stop_seq.second].apply( utl::overloaded{[&](n::location_group_idx_t const g) { return tt.get_default_translation( tt.location_group_name_[g]); }, [&](n::flex_area_idx_t const a) { return tt.get_default_translation( tt.flex_area_name_[a]); }})), UTL_GET_TIMING_MS(routing_timer)); } for (auto& [_, offsets] : ret) { utl::sort(offsets, [](n::routing::td_offset const& a, n::routing::td_offset const& b) { return a.valid_from_ < b.valid_from_; }); } } } // namespace motis::flex ================================================ FILE: src/flex/flex_areas.cc ================================================ #include "motis/flex/flex_areas.h" #include "osr/lookup.h" #include "utl/concat.h" #include "utl/parallel_for.h" #include "nigiri/timetable.h" namespace n = nigiri; namespace motis::flex { tg_ring* convert_ring(std::vector& ring_tmp, auto&& osm_ring) { ring_tmp.clear(); for (auto const& p : osm_ring) { ring_tmp.emplace_back(tg_point{p.lng_, p.lat_}); } return tg_ring_new(ring_tmp.data(), static_cast(ring_tmp.size())); } flex_areas::~flex_areas() { for (auto const& mp : idx_) { tg_geom_free(mp); } } flex_areas::flex_areas(nigiri::timetable const& tt, osr::ways const& w, osr::lookup const& l) { struct tmp { std::vector ring_tmp_; std::vector inner_tmp_; std::vector polys_tmp_; basic_string areas_; }; area_nodes_.resize(tt.flex_area_outers_.size()); idx_.resize(tt.flex_area_outers_.size()); utl::parallel_for_run_threadlocal( tt.flex_area_outers_.size(), [&](tmp& tmp, std::size_t const i) { tmp.polys_tmp_.clear(); auto const a = n::flex_area_idx_t{i}; auto const& outer_rings = tt.flex_area_outers_[a]; auto box = geo::box{}; for (auto const [outer_idx, outer_ring] : utl::enumerate(outer_rings)) { tmp.inner_tmp_.clear(); for (auto const inner_ring : tt.flex_area_inners_[a][static_cast(outer_idx)]) { tmp.inner_tmp_.emplace_back( convert_ring(tmp.ring_tmp_, inner_ring)); } for (auto const& c : outer_ring) { box.extend(c); } auto const outer = convert_ring(tmp.ring_tmp_, outer_ring); auto const poly = tg_poly_new(outer, tmp.inner_tmp_.data(), static_cast(tmp.inner_tmp_.size())); tg_ring_free(outer); tmp.polys_tmp_.emplace_back(poly); } idx_[a] = tg_geom_new_multipolygon( tmp.polys_tmp_.data(), static_cast(tmp.polys_tmp_.size())); auto b = osr::bitvec{}; b.resize(w.n_nodes()); l.find(tt.flex_area_bbox_[a], [&](osr::way_idx_t const way) { for (auto const& x : w.r_->way_nodes_[way]) { if (is_in_area(a, w.get_node_pos(x).as_latlng())) { b.set(x, true); } } }); area_nodes_[a] = gbfs::compress_bitvec(b); for (auto const& x : tmp.inner_tmp_) { tg_ring_free(x); } for (auto const x : tmp.polys_tmp_) { tg_poly_free(x); } }); } bool flex_areas::is_in_area(nigiri::flex_area_idx_t const a, geo::latlng const& c) const { auto const point = tg_geom_new_point(tg_point{c.lng(), c.lat()}); auto const result = tg_geom_within(point, idx_[a]); tg_geom_free(point); return result; } void flex_areas::add_area(nigiri::flex_area_idx_t a, osr::bitvec& b, osr::bitvec& tmp) const { gbfs::decompress_bitvec(area_nodes_[a], tmp); tmp.for_each_set_bit([&](auto&& i) { b.set(osr::node_idx_t{i}, true); }); } } // namespace motis::flex ================================================ FILE: src/flex/flex_output.cc ================================================ #include "motis/flex/flex_output.h" #include "nigiri/flex.h" #include "nigiri/timetable.h" #include "motis/flex/flex.h" #include "motis/flex/flex_areas.h" #include "motis/flex/flex_routing_data.h" #include "motis/osr/street_routing.h" #include "motis/place.h" namespace n = nigiri; namespace motis::flex { std::string_view get_flex_stop_name(n::timetable const& tt, n::lang_t const& lang, n::flex_stop_t const& s) { return s.apply( utl::overloaded{[&](n::flex_area_idx_t const a) { return tt.translate(lang, tt.flex_area_name_[a]); }, [&](n::location_group_idx_t const lg) { return tt.translate(lang, tt.location_group_name_[lg]); }}); } std::string_view get_flex_id(n::timetable const& tt, n::flex_stop_t const& s) { return s.apply(utl::overloaded{[&](n::flex_area_idx_t const a) { return tt.strings_.get(tt.flex_area_id_[a]); }, [&](n::location_group_idx_t const lg) { return tt.strings_.get( tt.location_group_id_[lg]); }}); } flex_output::flex_output(osr::ways const& w, osr::lookup const& l, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const* ae, tz_map_t const* tz, tag_lookup const& tags, n::timetable const& tt, flex_areas const& fa, mode_id const id) : w_{w}, pl_{pl}, matches_{matches}, ae_{ae}, tz_{tz}, tt_{tt}, tags_{tags}, fa_{fa}, sharing_data_{flex::prepare_sharing_data( tt, w, l, pl, fa, matches, id, id.get_dir(), flex_routing_data_)}, mode_id_(id) {} flex_output::~flex_output() = default; bool flex_output::is_time_dependent() const { return false; } api::ModeEnum flex_output::get_mode() const { return api::ModeEnum::FLEX; } transport_mode_t flex_output::get_cache_key() const { return mode_id_.to_id(); } osr::search_profile flex_output::get_profile() const { return osr::search_profile::kCarSharing; } osr::sharing_data const* flex_output::get_sharing_data() const { return &sharing_data_; } void flex_output::annotate_leg(n::lang_t const& lang, osr::node_idx_t const from, osr::node_idx_t const to, api::Leg& leg) const { if (from == osr::node_idx_t::invalid() || to == osr::node_idx_t::invalid()) { return; } auto const t = mode_id_.get_flex_transport(); auto const stop_seq = tt_.flex_stop_seq_[tt_.flex_transport_stop_seq_[t]]; auto from_stop = std::optional{}; auto to_stop = std::optional{}; for (auto i = 0U; i != stop_seq.size(); ++i) { auto const stop_idx = static_cast( mode_id_.get_dir() == osr::direction::kForward ? i : stop_seq.size() - i - 1U); auto const stop = stop_seq[stop_idx]; if (!from_stop.has_value() && is_in_flex_stop(tt_, w_, fa_, flex_routing_data_, stop, from)) { from_stop = stop_idx; } else if (!to_stop.has_value() && is_in_flex_stop(tt_, w_, fa_, flex_routing_data_, stop, to)) { to_stop = stop_idx; break; } } if (!from_stop.has_value()) { n::log(n::log_lvl::error, "flex", "flex: from [node={}] not found", from); return; } if (!to_stop.has_value()) { n::log(n::log_lvl::error, "flex", "flex: to [node={}] not found", to); return; } auto const write_node_info = [&](api::Place& p, osr::node_idx_t const n) { if (w_.is_additional_node(n)) { auto const l = flex_routing_data_.get_additional_node(n); p = to_place(&tt_, &tags_, &w_, pl_, matches_, ae_, tz_, lang, tt_location{l}); } }; write_node_info(leg.from_, from); write_node_info(leg.to_, to); leg.mode_ = api::ModeEnum::FLEX; leg.from_.flex_ = get_flex_stop_name(tt_, lang, stop_seq[*from_stop]); leg.from_.flexId_ = get_flex_id(tt_, stop_seq[*from_stop]); leg.to_.flex_ = get_flex_stop_name(tt_, lang, stop_seq[*to_stop]); leg.to_.flexId_ = get_flex_id(tt_, stop_seq[*to_stop]); auto const time_windows = tt_.flex_transport_stop_time_windows_[t]; leg.from_.flexStartPickupDropOffWindow_ = std::chrono::time_point_cast(leg.startTime_.time_) + time_windows[*from_stop].from_; leg.from_.flexEndPickupDropOffWindow_ = std::chrono::time_point_cast(leg.startTime_.time_) + time_windows[*from_stop].to_; leg.to_.flexStartPickupDropOffWindow_ = std::chrono::time_point_cast(leg.endTime_.time_) + time_windows[*to_stop].from_; leg.to_.flexEndPickupDropOffWindow_ = std::chrono::time_point_cast(leg.endTime_.time_) + time_windows[*to_stop].to_; } api::Place flex_output::get_place(n::lang_t const& lang, osr::node_idx_t const n, std::optional const& tz) const { if (w_.is_additional_node(n)) { auto const l = flex_routing_data_.get_additional_node(n); auto const c = tt_.locations_.coordinates_.at(l); return api::Place{ .name_ = std::string{tt_.translate(lang, tt_.locations_.names_.at(l))}, .lat_ = c.lat_, .lon_ = c.lng_, .tz_ = tz, .vertexType_ = api::VertexTypeEnum::TRANSIT}; } else { auto const pos = w_.get_node_pos(n).as_latlng(); return api::Place{.lat_ = pos.lat_, .lon_ = pos.lng_, .tz_ = tz, .vertexType_ = api::VertexTypeEnum::NORMAL}; } } } // namespace motis::flex ================================================ FILE: src/gbfs/data.cc ================================================ #include "motis/gbfs/data.h" #include "osr/lookup.h" #include "osr/ways.h" #include "motis/gbfs/compression.h" #include "motis/gbfs/routing_data.h" namespace motis::gbfs { products_routing_data::products_routing_data( std::shared_ptr&& prd, compressed_routing_data const& compressed) : provider_routing_data_{std::move(prd)}, compressed_{compressed} { decompress_bitvec(compressed_.start_allowed_, start_allowed_); decompress_bitvec(compressed_.end_allowed_, end_allowed_); decompress_bitvec(compressed_.through_allowed_, through_allowed_); } std::shared_ptr gbfs_data::get_products_routing_data( osr::ways const& w, osr::lookup const& l, gbfs_products_ref const prod_ref) { auto lock = std::unique_lock{products_routing_data_mutex_}; if (auto it = products_routing_data_.find(prod_ref); it != end(products_routing_data_)) { if (auto prod_rd = it->second.lock(); prod_rd) { return prod_rd; } } auto provider_rd = get_provider_routing_data( w, l, *this, *providers_.at(prod_ref.provider_)); auto prod_rd = provider_rd->get_products_routing_data(prod_ref.products_); products_routing_data_[prod_ref] = prod_rd; return prod_rd; } } // namespace motis::gbfs ================================================ FILE: src/gbfs/gbfs_output.cc ================================================ #include "motis/gbfs/gbfs_output.h" #include "motis/gbfs/mode.h" #include "motis/gbfs/osr_profile.h" #include "motis/gbfs/routing_data.h" namespace motis::gbfs { gbfs_output::~gbfs_output() = default; gbfs_output::gbfs_output(osr::ways const& w, gbfs_routing_data& gbfs_rd, gbfs_products_ref const prod_ref, bool const ignore_rental_return_constraints) : w_{w}, gbfs_rd_{gbfs_rd}, provider_{*gbfs_rd_.data_->providers_.at(prod_ref.provider_)}, products_{provider_.products_.at(prod_ref.products_)}, prod_rd_{gbfs_rd_.get_products_routing_data(prod_ref)}, sharing_data_{prod_rd_->get_sharing_data( w_.n_nodes(), ignore_rental_return_constraints)}, rental_{ .providerId_ = provider_.id_, .providerGroupId_ = provider_.group_id_, .systemId_ = provider_.sys_info_.id_, .systemName_ = provider_.sys_info_.name_, .url_ = provider_.sys_info_.url_, .color_ = provider_.color_, .formFactor_ = to_api_form_factor(products_.form_factor_), .propulsionType_ = to_api_propulsion_type(products_.propulsion_type_), .returnConstraint_ = to_api_return_constraint(products_.return_constraint_)} {} api::ModeEnum gbfs_output::get_mode() const { return api::ModeEnum::RENTAL; } bool gbfs_output::is_time_dependent() const { return false; } transport_mode_t gbfs_output::get_cache_key() const { return gbfs_rd_.get_transport_mode({provider_.idx_, products_.idx_}); } osr::search_profile gbfs_output::get_profile() const { return get_osr_profile(products_.form_factor_); } osr::sharing_data const* gbfs_output::get_sharing_data() const { return &sharing_data_; } void gbfs_output::annotate_leg(nigiri::lang_t const&, osr::node_idx_t const from_node, osr::node_idx_t const to_node, api::Leg& leg) const { auto const from_additional_node = w_.is_additional_node(from_node); auto const to_additional_node = w_.is_additional_node(to_node); auto const is_rental = (leg.mode_ == api::ModeEnum::BIKE || leg.mode_ == api::ModeEnum::CAR) && (from_additional_node || to_additional_node); if (!is_rental) { return; } leg.rental_ = rental_; leg.mode_ = api::ModeEnum::RENTAL; auto& ret = *leg.rental_; if (w_.is_additional_node(from_node)) { auto const& an = prod_rd_->compressed_.additional_nodes_.at( get_additional_node_idx(from_node)); std::visit( utl::overloaded{ [&](additional_node::station const& s) { auto const& st = provider_.stations_.at(s.id_); ret.fromStationName_ = st.info_.name_; ret.stationName_ = st.info_.name_; ret.rentalUriAndroid_ = st.info_.rental_uris_.android_; ret.rentalUriIOS_ = st.info_.rental_uris_.ios_; ret.rentalUriWeb_ = st.info_.rental_uris_.web_; }, [&](additional_node::vehicle const& v) { auto const& vs = provider_.vehicle_status_.at(v.idx_); if (auto const st = provider_.stations_.find(vs.station_id_); st != end(provider_.stations_)) { ret.fromStationName_ = st->second.info_.name_; } ret.rentalUriAndroid_ = vs.rental_uris_.android_; ret.rentalUriIOS_ = vs.rental_uris_.ios_; ret.rentalUriWeb_ = vs.rental_uris_.web_; }}, an.data_); } if (w_.is_additional_node(to_node)) { auto const& an = prod_rd_->compressed_.additional_nodes_.at( get_additional_node_idx(to_node)); std::visit( utl::overloaded{[&](additional_node::station const& s) { auto const& st = provider_.stations_.at(s.id_); ret.toStationName_ = st.info_.name_; if (!ret.stationName_) { ret.stationName_ = ret.toStationName_; } }, [&](additional_node::vehicle const& v) { auto const& vs = provider_.vehicle_status_.at(v.idx_); if (auto const st = provider_.stations_.find(vs.station_id_); st != end(provider_.stations_)) { ret.toStationName_ = st->second.info_.name_; } }}, an.data_); } } api::Place gbfs_output::get_place(nigiri::lang_t const&, osr::node_idx_t const n, std::optional const& tz) const { if (w_.is_additional_node(n)) { auto const pos = get_sharing_data()->get_additional_node_coordinates(n); return api::Place{.name_ = get_node_name(n), .lat_ = pos.lat_, .lon_ = pos.lng_, .tz_ = tz, .vertexType_ = api::VertexTypeEnum::BIKESHARE}; } else { auto const pos = w_.get_node_pos(n).as_latlng(); return api::Place{.lat_ = pos.lat_, .lon_ = pos.lng_, .tz_ = tz, .vertexType_ = api::VertexTypeEnum::NORMAL}; } } std::string gbfs_output::get_node_name(osr::node_idx_t const n) const { auto const& an = prod_rd_->compressed_.additional_nodes_.at(get_additional_node_idx(n)); return std::visit( utl::overloaded{[&](additional_node::station const& s) { return provider_.stations_.at(s.id_).info_.name_; }, [&](additional_node::vehicle const& v) { auto const& vs = provider_.vehicle_status_.at(v.idx_); auto const it = provider_.stations_.find(vs.station_id_); return it == end(provider_.stations_) ? provider_.sys_info_.name_ : it->second.info_.name_; }}, an.data_); } std::size_t gbfs_output::get_additional_node_idx( osr::node_idx_t const n) const { return to_idx(n) - sharing_data_.additional_node_offset_; } } // namespace motis::gbfs ================================================ FILE: src/gbfs/geofencing.cc ================================================ #include "motis/gbfs/data.h" #include "utl/helpers/algorithm.h" #include "tg.h" namespace motis::gbfs { bool applies(std::vector const& rule_vehicle_type_idxs, std::vector const& segment_vehicle_type_idxs) { return rule_vehicle_type_idxs.empty() || utl::all_of(segment_vehicle_type_idxs, [&](auto const& idx) { return utl::find(rule_vehicle_type_idxs, idx) != end(rule_vehicle_type_idxs); }); } bool multipoly_contains_point(tg_geom const* geom, geo::latlng const& pos) { auto const n_polys = tg_geom_num_polys(geom); for (auto i = 0; i < n_polys; ++i) { auto const* poly = tg_geom_poly_at(geom, i); if (tg_geom_intersects_xy(reinterpret_cast(poly), pos.lng(), pos.lat())) { return true; } } return false; } geofencing_restrictions geofencing_zones::get_restrictions( geo::latlng const& pos, vehicle_type_idx_t const vehicle_type_idx, geofencing_restrictions const& default_restrictions) const { auto const check_vehicle_type = vehicle_type_idx != vehicle_type_idx_t::invalid(); for (auto const& z : zones_) { if (multipoly_contains_point(z.geom_.get(), pos)) { for (auto const& r : z.rules_) { if (check_vehicle_type && !r.vehicle_type_idxs_.empty() && utl::find(r.vehicle_type_idxs_, vehicle_type_idx) == end(r.vehicle_type_idxs_)) { continue; } return geofencing_restrictions{ .ride_start_allowed_ = r.ride_start_allowed_, .ride_end_allowed_ = r.ride_end_allowed_, .ride_through_allowed_ = r.ride_through_allowed_, .station_parking_ = r.station_parking_}; } } } return default_restrictions; } } // namespace motis::gbfs ================================================ FILE: src/gbfs/mode.cc ================================================ #include "motis/gbfs/mode.h" #include #include "utl/helpers/algorithm.h" #include "utl/verify.h" #include "motis/constants.h" namespace motis::gbfs { api::RentalFormFactorEnum to_api_form_factor(vehicle_form_factor const ff) { switch (ff) { case vehicle_form_factor::kBicycle: return api::RentalFormFactorEnum::BICYCLE; case vehicle_form_factor::kCargoBicycle: return api::RentalFormFactorEnum::CARGO_BICYCLE; case vehicle_form_factor::kCar: return api::RentalFormFactorEnum::CAR; case vehicle_form_factor::kMoped: return api::RentalFormFactorEnum::MOPED; case vehicle_form_factor::kScooterStanding: return api::RentalFormFactorEnum::SCOOTER_STANDING; case vehicle_form_factor::kScooterSeated: return api::RentalFormFactorEnum::SCOOTER_SEATED; case vehicle_form_factor::kOther: return api::RentalFormFactorEnum::OTHER; } std::unreachable(); } vehicle_form_factor from_api_form_factor(api::RentalFormFactorEnum const ff) { switch (ff) { case api::RentalFormFactorEnum::BICYCLE: return vehicle_form_factor::kBicycle; case api::RentalFormFactorEnum::CARGO_BICYCLE: return vehicle_form_factor::kCargoBicycle; case api::RentalFormFactorEnum::CAR: return vehicle_form_factor::kCar; case api::RentalFormFactorEnum::MOPED: return vehicle_form_factor::kMoped; case api::RentalFormFactorEnum::SCOOTER_STANDING: return vehicle_form_factor::kScooterStanding; case api::RentalFormFactorEnum::SCOOTER_SEATED: return vehicle_form_factor::kScooterSeated; case api::RentalFormFactorEnum::OTHER: return vehicle_form_factor::kOther; } throw utl::fail("invalid rental form factor"); } api::RentalPropulsionTypeEnum to_api_propulsion_type(propulsion_type const pt) { switch (pt) { case propulsion_type::kHuman: return api::RentalPropulsionTypeEnum::HUMAN; case propulsion_type::kElectricAssist: return api::RentalPropulsionTypeEnum::ELECTRIC_ASSIST; case propulsion_type::kElectric: return api::RentalPropulsionTypeEnum::ELECTRIC; case propulsion_type::kCombustion: return api::RentalPropulsionTypeEnum::COMBUSTION; case propulsion_type::kCombustionDiesel: return api::RentalPropulsionTypeEnum::COMBUSTION_DIESEL; case propulsion_type::kHybrid: return api::RentalPropulsionTypeEnum::HYBRID; case propulsion_type::kPlugInHybrid: return api::RentalPropulsionTypeEnum::PLUG_IN_HYBRID; case propulsion_type::kHydrogenFuelCell: return api::RentalPropulsionTypeEnum::HYDROGEN_FUEL_CELL; } std::unreachable(); } propulsion_type from_api_propulsion_type( api::RentalPropulsionTypeEnum const pt) { switch (pt) { case api::RentalPropulsionTypeEnum::HUMAN: return propulsion_type::kHuman; case api::RentalPropulsionTypeEnum::ELECTRIC_ASSIST: return propulsion_type::kElectricAssist; case api::RentalPropulsionTypeEnum::ELECTRIC: return propulsion_type::kElectric; case api::RentalPropulsionTypeEnum::COMBUSTION: return propulsion_type::kCombustion; case api::RentalPropulsionTypeEnum::COMBUSTION_DIESEL: return propulsion_type::kCombustionDiesel; case api::RentalPropulsionTypeEnum::HYBRID: return propulsion_type::kHybrid; case api::RentalPropulsionTypeEnum::PLUG_IN_HYBRID: return propulsion_type::kPlugInHybrid; case api::RentalPropulsionTypeEnum::HYDROGEN_FUEL_CELL: return propulsion_type::kHydrogenFuelCell; } throw utl::fail("invalid rental propulsion type"); } api::RentalReturnConstraintEnum to_api_return_constraint( return_constraint const rc) { switch (rc) { case return_constraint::kFreeFloating: return api::RentalReturnConstraintEnum::NONE; case return_constraint::kAnyStation: return api::RentalReturnConstraintEnum::ANY_STATION; case return_constraint::kRoundtripStation: return api::RentalReturnConstraintEnum::ROUNDTRIP_STATION; } std::unreachable(); } bool products_match( provider_products const& prod, std::optional> const& form_factors, std::optional> const& propulsion_types) { if (form_factors.has_value() && utl::find(*form_factors, to_api_form_factor(prod.form_factor_)) == end(*form_factors)) { return false; } if (propulsion_types.has_value() && utl::find(*propulsion_types, to_api_propulsion_type(prod.propulsion_type_)) == end(*propulsion_types)) { return false; } return true; } } // namespace motis::gbfs ================================================ FILE: src/gbfs/osr_mapping.cc ================================================ #include "motis/gbfs/osr_mapping.h" #include #include #include #include "tg.h" #include "geo/box.h" #include "osr/lookup.h" #include "osr/routing/profiles/foot.h" #include "osr/types.h" #include "osr/ways.h" #include "utl/enumerate.h" #include "utl/helpers/algorithm.h" #include "utl/to_vec.h" #include "utl/zip.h" #include "motis/constants.h" #include "motis/types.h" #include "motis/box_rtree.h" #include "motis/gbfs/compression.h" #include "motis/gbfs/data.h" #include "motis/gbfs/geofencing.h" namespace motis::gbfs { struct node_match { osr::way_candidate const& way() const { return wc_; } osr::node_candidate const& node() const { return left_ ? wc_.left_ : wc_.right_; } osr::way_candidate wc_; bool left_{}; }; struct osr_mapping { osr_mapping(osr::ways const& w, osr::lookup const& l, gbfs_provider const& provider) : w_{w}, l_{l}, provider_{provider} { products_data_.resize(provider.products_.size()); } void map_geofencing_zones() { auto const make_loc_bitvec = [&]() { auto bv = osr::bitvec{}; bv.resize(static_cast::size_type>( w_.n_nodes() + provider_.stations_.size() + provider_.vehicle_status_.size())); return bv; }; auto zone_rtree = box_rtree{}; for (auto const [i, z] : utl::enumerate(provider_.geofencing_zones_.zones_)) { zone_rtree.add(z.bounding_box(), i); } for (auto [prod, rd] : utl::zip(provider_.products_, products_data_)) { auto default_restrictions = provider_.default_restrictions_; rd.start_allowed_ = make_loc_bitvec(); rd.end_allowed_ = make_loc_bitvec(); rd.through_allowed_ = make_loc_bitvec(); // global rules for (auto const& r : provider_.geofencing_zones_.global_rules_) { if (!applies(r.vehicle_type_idxs_, prod.vehicle_types_)) { continue; } default_restrictions.ride_start_allowed_ = r.ride_start_allowed_; default_restrictions.ride_end_allowed_ = r.ride_end_allowed_; default_restrictions.ride_through_allowed_ = r.ride_through_allowed_; default_restrictions.station_parking_ = r.station_parking_; break; } if ((prod.return_constraint_ == return_constraint::kAnyStation || prod.return_constraint_ == return_constraint::kRoundtripStation) && (prod.known_return_constraint_ || provider_.geofencing_zones_.zones_.empty()) && !default_restrictions.station_parking_.has_value()) { default_restrictions.station_parking_ = true; } if (default_restrictions.ride_end_allowed_ && !default_restrictions.station_parking_.value_or(false)) { rd.end_allowed_.one_out(); } if (default_restrictions.ride_through_allowed_) { rd.through_allowed_.one_out(); } rd.station_parking_ = default_restrictions.station_parking_.value_or(false); } auto done = make_loc_bitvec(); auto zone_indices = std::vector{}; zone_indices.reserve(provider_.geofencing_zones_.zones_.size()); auto const handle_point = [&](osr::node_idx_t const n, geo::latlng const& pos) { for (auto [prod, rd] : utl::zip(provider_.products_, products_data_)) { auto start_allowed = std::optional{}; auto end_allowed = std::optional{}; auto through_allowed = std::optional{}; auto station_parking = rd.station_parking_; // zones have to be checked in the order they are defined zone_indices.clear(); zone_rtree.find(pos, [&](std::size_t const zone_idx) { zone_indices.push_back(zone_idx); }); utl::sort(zone_indices); for (auto const zone_idx : zone_indices) { auto const& z = provider_.geofencing_zones_.zones_[zone_idx]; // check if pos is inside the zone multipolygon if (multipoly_contains_point(z.geom_.get(), pos)) { for (auto const& r : z.rules_) { if (!applies(r.vehicle_type_idxs_, prod.vehicle_types_)) { continue; } if (r.station_parking_.has_value()) { station_parking = r.station_parking_.value(); } start_allowed = r.ride_start_allowed_; end_allowed = r.ride_end_allowed_ && !station_parking; through_allowed = r.ride_through_allowed_; break; } if (start_allowed.has_value()) { break; // for now } } } if (end_allowed.has_value()) { rd.end_allowed_.set(n, *end_allowed); } if (through_allowed.has_value()) { rd.through_allowed_.set(n, *through_allowed); } } }; auto const* osr_r = w_.r_.get(); for (auto const& z : provider_.geofencing_zones_.zones_) { l_.find(z.bounding_box(), [&](osr::way_idx_t const way) { for (auto const n : osr_r->way_nodes_[way]) { if (done.test(n)) { continue; } done.set(n, true); handle_point(n, w_.get_node_pos(n).as_latlng()); } }); } } std::vector get_node_matches(osr::location const& loc) const { using footp = osr::bike_sharing::footp; using bikep = osr::bike_sharing::bikep; static constexpr auto foot_params = osr::bike_sharing::footp::parameters{}; static constexpr auto bike_params = osr::bike_sharing::bikep::parameters{}; auto is_acceptable_node = [&](osr::node_candidate const& n) { if (!n.valid() || n.dist_to_node_ > kMaxGbfsMatchingDistance) { return false; } auto const& node_props = w_.r_->node_properties_[n.node_]; if (footp::node_cost(foot_params, node_props) == osr::kInfeasible || bikep::node_cost(bike_params, node_props) == osr::kInfeasible) { return false; } // node needs to have at least one way accessible by foot and one by bike return utl::any_of(w_.r_->node_ways_[n.node_], [&](auto const way_idx) { return footp::way_cost( footp::parameters{}, w_.r_->way_properties_[way_idx], osr::direction::kForward, 0U) != osr::kInfeasible; }) && utl::any_of(w_.r_->node_ways_[n.node_], [&](auto const way_idx) { return bikep::way_cost( bikep::parameters{}, w_.r_->way_properties_[way_idx], osr::direction::kForward, 0U) != osr::kInfeasible; }); }; auto const matches = l_.match(footp::parameters{}, loc, false, osr::direction::kForward, kMaxGbfsMatchingDistance, nullptr); auto node_matches = std::vector{}; for (auto const& m : matches) { if (is_acceptable_node(m.left_)) { node_matches.emplace_back(node_match{m, true}); } if (is_acceptable_node(m.right_)) { node_matches.emplace_back(node_match{m, false}); } } utl::sort(node_matches, [](auto const& a, auto const& b) { return a.node().dist_to_node_ < b.node().dist_to_node_; }); auto connected_components = hash_set{}; for (auto it = node_matches.begin(); it != node_matches.end();) { auto const component = w_.r_->way_component_[it->way().way_]; if (!connected_components.insert(component).second) { it = node_matches.erase(it); } else { ++it; } } return node_matches; } void map_stations() { for (auto [prod_b, rd_b] : utl::zip(provider_.products_, products_data_)) { auto& prod = prod_b; // fix for apple clang auto& rd = rd_b; for (auto const& [id, st] : provider_.stations_) { auto is_renting = st.status_.is_renting_ && st.status_.num_vehicles_available_ > 0; auto is_returning = st.status_.is_returning_; // if the station lists vehicles available by type, at least one of // the vehicle types included in the product segment must be available if (is_renting && !st.status_.vehicle_types_available_.empty()) { is_renting = utl::any_of( st.status_.vehicle_types_available_, [&](auto const& vt) { return vt.second != 0 && prod.includes_vehicle_type(vt.first); }); } // same for returning vehicles if (is_returning && !st.status_.vehicle_docks_available_.empty()) { is_returning = utl::any_of( st.status_.vehicle_docks_available_, [&](auto const& vt) { return vt.second != 0 && prod.includes_vehicle_type(vt.first); }); } if (!is_renting && !is_returning) { continue; } auto const matches = get_node_matches(osr::location{st.info_.pos_, osr::level_t{}}); if (matches.empty()) { continue; } auto const additional_node_id = add_node( rd, additional_node{additional_node::station{id}}, st.info_.pos_); if (is_renting) { rd.start_allowed_.set(additional_node_id, true); } if (is_returning) { rd.end_allowed_.set(additional_node_id, true); if (st.info_.station_area_) { auto const* geom = st.info_.station_area_.get(); auto const rect = tg_geom_rect(geom); auto const bb = geo::box{geo::latlng{rect.min.y, rect.min.x}, geo::latlng{rect.max.y, rect.max.x}}; auto const* osr_r = w_.r_.get(); l_.find(bb, [&](osr::way_idx_t const way) { for (auto const n : osr_r->way_nodes_[way]) { if (multipoly_contains_point(geom, w_.get_node_pos(n).as_latlng())) { rd.end_allowed_.set(n, true); } } }); } } for (auto const& m : matches) { auto const& node = m.node(); auto const edge_to_an = osr::additional_edge{ additional_node_id, static_cast(node.dist_to_node_)}; auto& node_edges = rd.additional_edges_[node.node_]; if (utl::find(node_edges, edge_to_an) == end(node_edges)) { node_edges.emplace_back(edge_to_an); } auto const edge_from_an = osr::additional_edge{ node.node_, static_cast(node.dist_to_node_)}; auto& an_edges = rd.additional_edges_[additional_node_id]; if (utl::find(an_edges, edge_from_an) == end(an_edges)) { an_edges.emplace_back(edge_from_an); } } } } } void map_vehicles() { for (auto [prod, rd] : utl::zip(provider_.products_, products_data_)) { for (auto const [vehicle_idx, vs] : utl::enumerate(provider_.vehicle_status_)) { if (vs.is_disabled_ || vs.is_reserved_ || !prod.includes_vehicle_type(vs.vehicle_type_idx_)) { continue; } auto const restrictions = provider_.geofencing_zones_.get_restrictions( vs.pos_, vs.vehicle_type_idx_, geofencing_restrictions{}); if (!restrictions.ride_start_allowed_) { continue; } auto const matches = get_node_matches(osr::location{vs.pos_, osr::level_t{}}); if (matches.empty()) { continue; } auto const additional_node_id = add_node(rd, additional_node{additional_node::vehicle{vehicle_idx}}, vs.pos_); rd.start_allowed_.set(additional_node_id, true); for (auto const& m : matches) { auto const& nc = m.node(); auto const edge_to_an = osr::additional_edge{ additional_node_id, static_cast(nc.dist_to_node_)}; auto& node_edges = rd.additional_edges_[nc.node_]; if (utl::find(node_edges, edge_to_an) == end(node_edges)) { node_edges.push_back(edge_to_an); } auto const edge_from_an = osr::additional_edge{ nc.node_, static_cast(nc.dist_to_node_)}; auto& an_edges = rd.additional_edges_[additional_node_id]; if (utl::find(an_edges, edge_from_an) == end(an_edges)) { an_edges.push_back(edge_from_an); } } } } } osr::node_idx_t add_node(routing_data& rd, additional_node&& an, geo::latlng const& pos) const { auto const node_id = static_cast( w_.n_nodes() + rd.additional_nodes_.size()); rd.additional_nodes_.push_back(std::move(an)); rd.additional_node_coordinates_.push_back(pos); assert(rd.start_allowed_.size() >= static_cast::size_type>( node_id + 1)); return node_id; } osr::ways const& w_; osr::lookup const& l_; gbfs_provider const& provider_; std::vector products_data_; }; void map_data(osr::ways const& w, osr::lookup const& l, gbfs_provider const& provider, provider_routing_data& prd) { auto mapping = osr_mapping{w, l, provider}; mapping.map_geofencing_zones(); mapping.map_stations(); mapping.map_vehicles(); prd.products_ = utl::to_vec(mapping.products_data_, [&](routing_data& rd) { return compressed_routing_data{ .additional_nodes_ = std::move(rd.additional_nodes_), .additional_node_coordinates_ = std::move(rd.additional_node_coordinates_), .additional_edges_ = std::move(rd.additional_edges_), .start_allowed_ = compress_bitvec(rd.start_allowed_), .end_allowed_ = compress_bitvec(rd.end_allowed_), .through_allowed_ = compress_bitvec(rd.through_allowed_)}; }); } } // namespace motis::gbfs ================================================ FILE: src/gbfs/osr_profile.cc ================================================ #include "motis/gbfs/osr_profile.h" namespace motis::gbfs { osr::search_profile get_osr_profile(vehicle_form_factor const& ff) { return ff == vehicle_form_factor::kCar ? osr::search_profile::kCarSharing : osr::search_profile::kBikeSharing; } } // namespace motis::gbfs ================================================ FILE: src/gbfs/parser.cc ================================================ #include #include #include #include "motis/gbfs/parser.h" #include "cista/hash.h" #include "utl/helpers/algorithm.h" #include "utl/raii.h" #include "utl/to_vec.h" namespace json = boost::json; namespace motis::gbfs { gbfs_version get_version(json::value const& root) { auto const& root_obj = root.as_object(); if (!root_obj.contains("version")) { // 1.0 doesn't have the version key return gbfs_version::k1; } auto const version = static_cast(root.at("version").as_string()); if (version.starts_with("1.")) { return gbfs_version::k1; } else if (version.starts_with("2.")) { return gbfs_version::k2; } else if (version.starts_with("3.")) { return gbfs_version::k3; } else { throw utl::fail("unsupported GBFS version: {}", version); } } std::string get_localized_string(json::value const& v) { if (v.is_array()) { auto const& arr = v.as_array(); if (!arr.empty()) { return static_cast( arr[0].as_object().at("text").as_string()); } return ""; } else if (v.is_string()) { return static_cast(v.as_string()); } else { return ""; } } std::string get_as_string(json::object const& obj, std::string_view const key) { auto const val = obj.at(key); if (val.is_string()) { return static_cast(val.as_string()); } else if (val.is_int64()) { return std::to_string(val.as_int64()); } else if (val.is_uint64()) { return std::to_string(val.as_uint64()); } else { return json::serialize(val); } } std::string optional_str(json::object const& obj, std::string_view key) { return obj.contains(key) ? get_as_string(obj, key) : ""; } std::string optional_localized_str(json::object const& obj, std::string_view key) { return obj.contains(key) ? get_localized_string(obj.at(key)) : ""; } bool get_bool(json::object const& obj, std::string_view const key, std::optional const def = std::nullopt) { if (!obj.contains(key) && def.has_value()) { return *def; } auto const val = obj.at(key); if (val.is_bool()) { return val.as_bool(); } else if (val.is_number()) { return val.to_number() == 1; } else { return *def; } } tg_geom* parse_multipolygon(json::object const& json) { utl::verify(json.at("type").as_string() == "MultiPolygon", "expected MultiPolygon, got {}", json.at("type").as_string()); auto const& coordinates = json.at("coordinates").as_array(); auto polys = std::vector{}; UTL_FINALLY([&polys]() { for (auto const poly : polys) { tg_poly_free(poly); } }) for (auto const& j_poly : coordinates) { auto rings = std::vector{}; UTL_FINALLY([&rings]() { for (auto const ring : rings) { tg_ring_free(ring); } }) for (auto const& j_ring : j_poly.as_array()) { auto points = utl::to_vec(j_ring.as_array(), [&](auto const& j_pt) { auto const& j_pt_arr = j_pt.as_array(); utl::verify(j_pt_arr.size() >= 2, "invalid point in polygon ring"); return tg_point{j_pt_arr[0].as_double(), j_pt_arr[1].as_double()}; }); utl::verify(points.size() > 2, "empty ring in polygon"); // handle invalid polygons that don't have closed rings if (points.front().x != points.back().x || points.front().y != points.back().y) { points.push_back(points.front()); } auto ring = tg_ring_new(points.data(), static_cast(points.size())); utl::verify(ring != nullptr, "failed to create ring"); rings.emplace_back(ring); } utl::verify(!rings.empty(), "empty polygon in multipolygon"); auto poly = tg_poly_new(rings.front(), rings.size() > 1 ? &rings[1] : nullptr, static_cast(rings.size() - 1)); utl::verify(poly != nullptr, "failed to create polygon"); polys.emplace_back(poly); } utl::verify(!polys.empty(), "empty multipolygon"); auto const multipoly = tg_geom_new_multipolygon(polys.data(), static_cast(polys.size())); utl::verify(multipoly != nullptr, "failed to create multipolygon"); return multipoly; } hash_map parse_discovery(json::value const& root) { auto urls = hash_map{}; auto const& data = root.at("data").as_object(); if (data.empty()) { return urls; } auto const& feeds = data.contains("feeds") ? data.at("feeds").as_array() : data.begin()->value().as_object().at("feeds").as_array(); for (auto const& feed : feeds) { auto const& name = static_cast(feed.as_object().at("name").as_string()); auto const& url = static_cast(feed.as_object().at("url").as_string()); urls[name] = url; } return urls; } rental_uris parse_rental_uris(json::object const& parent) { auto uris = rental_uris{}; if (parent.contains("rental_uris")) { auto const& o = parent.at("rental_uris").as_object(); uris.android_ = optional_str(o, "android"); uris.ios_ = optional_str(o, "ios"); uris.web_ = optional_str(o, "web"); } return uris; } std::optional get_vehicle_type( gbfs_provider& provider, std::string const& vehicle_type_id, vehicle_start_type const start_type) { auto const add_vehicle_type = [&](vehicle_form_factor const ff, propulsion_type const pt, std::string const& name) { auto const idx = vehicle_type_idx_t{provider.vehicle_types_.size()}; provider.vehicle_types_.emplace_back(vehicle_type{ .id_ = vehicle_type_id, .idx_ = idx, .name_ = name, .form_factor_ = ff, .propulsion_type_ = pt, .return_constraint_ = provider.default_return_constraint_.value_or( start_type == vehicle_start_type::kStation ? return_constraint::kAnyStation : return_constraint::kFreeFloating), .known_return_constraint_ = false}); provider.vehicle_types_map_[{vehicle_type_id, start_type}] = idx; return idx; }; if (auto const it = provider.vehicle_types_map_.find({vehicle_type_id, start_type}); it != end(provider.vehicle_types_map_)) { return it->second; } else if (auto const temp_it = provider.temp_vehicle_types_.find(vehicle_type_id); temp_it != end(provider.temp_vehicle_types_)) { return add_vehicle_type(temp_it->second.form_factor_, temp_it->second.propulsion_type_, temp_it->second.name_); } else if (vehicle_type_id.empty()) { // providers that don't use vehicle types return add_vehicle_type(vehicle_form_factor::kBicycle, propulsion_type::kHuman, ""); } return {}; } void load_system_information(gbfs_provider& provider, json::value const& root) { auto const& data = root.at("data").as_object(); auto& si = provider.sys_info_; si.id_ = static_cast(data.at("system_id").as_string()); si.name_ = get_localized_string(data.at("name")); si.name_short_ = optional_localized_str(data, "name_short"); si.operator_ = optional_localized_str(data, "operator"); si.url_ = optional_str(data, "url"); si.purchase_url_ = optional_str(data, "purchase_url"); si.mail_ = optional_str(data, "email"); if (data.contains("brand_assets")) { auto const& ba = data.at("brand_assets").as_object(); si.color_ = optional_str(ba, "color"); } else { si.color_ = ""; } } void load_station_information(gbfs_provider& provider, json::value const& root) { provider.stations_.clear(); auto const& stations_arr = root.at("data").at("stations").as_array(); for (auto const& s : stations_arr) { auto const& station_obj = s.as_object(); auto const station_id = get_as_string(station_obj, "station_id"); try { auto const name = get_localized_string(station_obj.at("name")); auto const lat = station_obj.at("lat").as_double(); auto const lon = station_obj.at("lon").as_double(); tg_geom* area = nullptr; if (station_obj.contains("station_area")) { try { area = parse_multipolygon(station_obj.at("station_area").as_object()); } catch (std::exception const& ex) { std::cerr << "[GBFS] (" << provider.id_ << ") invalid station_area: " << ex.what() << "\n"; } } provider.stations_[station_id] = station{ .info_ = {.id_ = station_id, .name_ = name, .pos_ = geo::latlng{lat, lon}, .address_ = optional_str(station_obj, "address"), .cross_street_ = optional_str(station_obj, "cross_street"), .rental_uris_ = parse_rental_uris(station_obj), .station_area_ = std::shared_ptr(area, tg_geom_deleter{})}}; } catch (std::exception const& ex) { std::cerr << "[GBFS] (" << provider.id_ << ") error parsing station " << station_id << ": " << ex.what() << "\n"; } } } void load_station_status(gbfs_provider& provider, json::value const& root) { auto const& stations_arr = root.at("data").at("stations").as_array(); for (auto const& s : stations_arr) { auto const& station_obj = s.as_object(); auto const station_id = get_as_string(station_obj, "station_id"); auto const station_it = provider.stations_.find(station_id); if (station_it == end(provider.stations_)) { continue; } auto& station = station_it->second; station.status_ = station_status{ .num_vehicles_available_ = 0U, .is_renting_ = get_bool(station_obj, "is_renting", true), .is_returning_ = get_bool(station_obj, "is_returning", true)}; if (station_obj.contains("num_vehicles_available")) { // GBFS 3.x (but some 2.x feeds use this as well) station.status_.num_vehicles_available_ = station_obj.at("num_vehicles_available").to_number(); } else if (station_obj.contains("num_bikes_available")) { // GBFS 2.x station.status_.num_vehicles_available_ = station_obj.at("num_bikes_available").to_number(); } if (station_obj.contains("vehicle_types_available")) { auto const& vta = station_obj.at("vehicle_types_available").as_array(); auto unrestricted_available = 0U; auto any_station_available = 0U; auto roundtrip_available = 0U; for (auto const& vt : vta) { auto const vehicle_type_id = static_cast(vt.at("vehicle_type_id").as_string()); auto const count = vt.at("count").to_number(); if (auto const vt_idx = get_vehicle_type(provider, vehicle_type_id, vehicle_start_type::kStation); vt_idx) { station.status_.vehicle_types_available_[*vt_idx] = count; switch (provider.vehicle_types_[*vt_idx].return_constraint_) { case return_constraint::kFreeFloating: unrestricted_available += count; break; case return_constraint::kAnyStation: any_station_available += count; break; case return_constraint::kRoundtripStation: roundtrip_available += count; break; } } } station.status_.num_vehicles_available_ = unrestricted_available + any_station_available + roundtrip_available; } else { if (auto const vt_idx = get_vehicle_type(provider, "", vehicle_start_type::kStation); vt_idx) { station.status_.vehicle_types_available_[*vt_idx] = station.status_.num_vehicles_available_; } } if (station_obj.contains("vehicle_docks_available")) { for (auto const& vt : station_obj.at("vehicle_docks_available").as_array()) { auto& vto = vt.as_object(); if (vto.contains("vehicle_type_ids") && vto.contains("count")) { for (auto const& vti : vto.at("vehicle_type_ids").as_array()) { auto const vehicle_type_id = static_cast(vti.as_string()); if (auto const vt_idx = get_vehicle_type( provider, vehicle_type_id, vehicle_start_type::kStation); vt_idx) { station.status_.vehicle_docks_available_[*vt_idx] = vto.at("count").to_number(); } } } } } } } vehicle_form_factor parse_form_factor(std::string_view const s) { switch (cista::hash(s)) { case cista::hash("bicycle"): case cista::hash("bike"): // non-standard return vehicle_form_factor::kBicycle; case cista::hash("cargo_bicycle"): return vehicle_form_factor::kCargoBicycle; case cista::hash("car"): return vehicle_form_factor::kCar; case cista::hash("moped"): return vehicle_form_factor::kMoped; case cista::hash("scooter"): // < 3.0 case cista::hash("scooter_standing"): return vehicle_form_factor::kScooterStanding; case cista::hash("scooter_seated"): return vehicle_form_factor::kScooterSeated; case cista::hash("other"): default: return vehicle_form_factor::kOther; } } propulsion_type parse_propulsion_type(std::string_view const s) { switch (cista::hash(s)) { case cista::hash("human"): return propulsion_type::kHuman; case cista::hash("electric_assist"): return propulsion_type::kElectricAssist; case cista::hash("electric"): return propulsion_type::kElectric; case cista::hash("combustion"): return propulsion_type::kCombustion; case cista::hash("combustion_diesel"): return propulsion_type::kCombustionDiesel; case cista::hash("hybrid"): return propulsion_type::kHybrid; case cista::hash("plug_in_hybrid"): return propulsion_type::kPlugInHybrid; case cista::hash("hydrogen_fuel_cell"): return propulsion_type::kHydrogenFuelCell; default: return propulsion_type::kHuman; } } std::optional parse_return_constraint( std::string_view const s) { switch (cista::hash(s)) { case cista::hash("any_station"): return return_constraint::kAnyStation; case cista::hash("roundtrip_station"): return return_constraint::kRoundtripStation; case cista::hash("free_floating"): case cista::hash("hybrid"): return return_constraint::kFreeFloating; default: return {}; } } std::optional parse_return_constraint( json::object const& vt) { if (vt.contains("return_constraint")) { return parse_return_constraint(vt.at("return_constraint").as_string()); } return {}; } void load_vehicle_types(gbfs_provider& provider, json::value const& root) { provider.vehicle_types_.clear(); provider.vehicle_types_map_.clear(); provider.temp_vehicle_types_.clear(); for (auto const& v : root.at("data").at("vehicle_types").as_array()) { auto const id = static_cast(v.at("vehicle_type_id").as_string()); auto const name = optional_localized_str(v.as_object(), "name"); auto const rc = parse_return_constraint(v.as_object()); auto const form_factor = parse_form_factor(optional_str(v.as_object(), "form_factor")); auto const propulsion_type = parse_propulsion_type(optional_str(v.as_object(), "propulsion_type")); if (rc) { auto const idx = vehicle_type_idx_t{provider.vehicle_types_.size()}; provider.vehicle_types_.emplace_back( vehicle_type{.id_ = id, .idx_ = idx, .name_ = name, .form_factor_ = form_factor, .propulsion_type_ = propulsion_type, .return_constraint_ = *rc, .known_return_constraint_ = true}); provider.vehicle_types_map_[{id, vehicle_start_type::kStation}] = idx; provider.vehicle_types_map_[{id, vehicle_start_type::kFreeFloating}] = idx; } else { provider.temp_vehicle_types_[id] = temp_vehicle_type{ .id_ = id, .name_ = name, .form_factor_ = form_factor, .propulsion_type_ = propulsion_type, }; } } } void load_vehicle_status(gbfs_provider& provider, json::value const& root) { provider.vehicle_status_.clear(); auto const version = get_version(root); auto const& vehicles_arr = root.at("data") .at(version == gbfs_version::k3 ? "vehicles" : "bikes") .as_array(); for (auto const& v : vehicles_arr) { auto const& vehicle_obj = v.as_object(); auto pos = geo::latlng{}; if (vehicle_obj.contains("lat") && vehicle_obj.contains("lon")) { auto const lat = vehicle_obj.at("lat"); auto const lon = vehicle_obj.at("lon"); if (!lat.is_double() || !lon.is_double()) { continue; } pos = geo::latlng{vehicle_obj.at("lat").as_double(), vehicle_obj.at("lon").as_double()}; } else if (vehicle_obj.contains("station_id")) { auto const station_id = get_as_string(vehicle_obj, "station_id"); if (auto const it = provider.stations_.find(station_id); it != end(provider.stations_)) { pos = it->second.info_.pos_; } else { continue; } } else { continue; } auto const id = get_as_string( vehicle_obj, version == gbfs_version::k3 ? "vehicle_id" : "bike_id"); auto const type_id = optional_str(vehicle_obj, "vehicle_type_id"); auto type_idx = vehicle_type_idx_t::invalid(); if (auto const vt_idx = get_vehicle_type(provider, type_id, vehicle_start_type::kFreeFloating); vt_idx) { type_idx = *vt_idx; } provider.vehicle_status_.emplace_back(vehicle_status{ .id_ = id, .pos_ = pos, .is_reserved_ = get_bool(vehicle_obj, "is_reserved", false), .is_disabled_ = get_bool(vehicle_obj, "is_disabled", false), .vehicle_type_idx_ = type_idx, .station_id_ = optional_str(vehicle_obj, "station_id"), .home_station_id_ = optional_str(vehicle_obj, "home_station_id"), .rental_uris_ = parse_rental_uris(vehicle_obj)}); } utl::sort(provider.vehicle_status_); } rule parse_rule(gbfs_provider& provider, gbfs_version const version, json::value const& r) { auto const vti_key = version == gbfs_version::k2 ? "vehicle_type_id" : "vehicle_type_ids"; auto const& rule_obj = r.as_object(); auto vehicle_type_idxs = std::vector{}; if (rule_obj.contains(vti_key)) { for (auto const& vt : rule_obj.at(vti_key).as_array()) { auto const vt_id = static_cast(vt.as_string()); if (auto const it = provider.vehicle_types_map_.find( {vt_id, vehicle_start_type::kStation}); it != end(provider.vehicle_types_map_)) { vehicle_type_idxs.emplace_back(it->second); } if (auto const it = provider.vehicle_types_map_.find( {vt_id, vehicle_start_type::kFreeFloating}); it != end(provider.vehicle_types_map_)) { vehicle_type_idxs.emplace_back(it->second); } } } return rule{ .vehicle_type_idxs_ = std::move(vehicle_type_idxs), .ride_start_allowed_ = version == gbfs_version::k2 ? rule_obj.at("ride_allowed").as_bool() : rule_obj.at("ride_start_allowed").as_bool(), .ride_end_allowed_ = version == gbfs_version::k2 ? rule_obj.at("ride_allowed").as_bool() : rule_obj.at("ride_end_allowed").as_bool(), .ride_through_allowed_ = rule_obj.at("ride_through_allowed").as_bool(), .station_parking_ = rule_obj.contains("station_parking") ? std::optional{rule_obj.at("station_parking").as_bool()} : std::nullopt}; } void load_geofencing_zones(gbfs_provider& provider, json::value const& root) { auto const version = get_version(root); auto const& zones_obj = root.at("data").at("geofencing_zones").as_object(); utl::verify(zones_obj.at("type") == "FeatureCollection", "invalid geofencing_zones"); auto zones = std::vector{}; auto const zones_arr = zones_obj.at("features").as_array(); zones.reserve(zones_arr.size()); for (auto const& z : zones_arr) { try { auto const& props = z.at("properties").as_object(); if (!props.contains("rules") || !props.at("rules").is_array()) { continue; } auto rules = utl::to_vec( props.at("rules").as_array(), [&](auto const& r) { return parse_rule(provider, version, r); }); auto* geom = parse_multipolygon(z.at("geometry").as_object()); auto name = optional_localized_str(props, "name"); zones.emplace_back(geom, std::move(rules), std::move(name)); } catch (std::exception const& ex) { std::cerr << "[GBFS] (" << provider.id_ << ") invalid geofencing zone: " << ex.what() << "\n"; } } // required in 3.0, but some feeds don't have it auto global_rules = root.at("data").as_object().contains("global_rules") && root.at("data").at("global_rules").is_array() ? utl::to_vec( root.at("data").at("global_rules").as_array(), [&](auto const& r) { return parse_rule(provider, version, r); }) : std::vector{}; provider.geofencing_zones_.version_ = version; provider.geofencing_zones_.zones_ = std::move(zones); provider.geofencing_zones_.global_rules_ = std::move(global_rules); } } // namespace motis::gbfs ================================================ FILE: src/gbfs/routing_data.cc ================================================ #include "motis/gbfs/routing_data.h" #include "osr/lookup.h" #include "osr/types.h" #include "osr/ways.h" #include "fmt/format.h" #include "utl/get_or_create.h" #include "utl/timer.h" #include "motis/constants.h" #include "motis/gbfs/data.h" #include "motis/gbfs/osr_mapping.h" #include "motis/transport_mode_ids.h" namespace motis::gbfs { std::shared_ptr compute_provider_routing_data( osr::ways const& w, osr::lookup const& l, gbfs_provider const& provider) { auto timer = utl::scoped_timer{ fmt::format("compute routing data for gbfs provider {}", provider.id_)}; auto prd = std::make_shared(); map_data(w, l, provider, *prd); return prd; } std::shared_ptr get_provider_routing_data( osr::ways const& w, osr::lookup const& l, gbfs_data& data, gbfs_provider const& provider) { return data.cache_.get_or_compute(provider.idx_, [&]() { return compute_provider_routing_data(w, l, provider); }); } std::shared_ptr gbfs_routing_data::get_provider_routing_data(gbfs_provider const& provider) { return gbfs::get_provider_routing_data(*w_, *l_, *data_, provider); } products_routing_data* gbfs_routing_data::get_products_routing_data( gbfs_provider const& provider, gbfs_products_idx_t const prod_idx) { auto const ref = gbfs::gbfs_products_ref{provider.idx_, prod_idx}; return utl::get_or_create( products_, ref, [&] { return data_->get_products_routing_data(*w_, *l_, ref); }) .get(); } products_routing_data* gbfs_routing_data::get_products_routing_data( gbfs_products_ref const prod_ref) { return get_products_routing_data(*data_->providers_.at(prod_ref.provider_), prod_ref.products_); } provider_products const& gbfs_routing_data::get_products( gbfs_products_ref const prod_ref) { return data_->providers_.at(prod_ref.provider_) ->products_.at(prod_ref.products_); } nigiri::transport_mode_id_t gbfs_routing_data::get_transport_mode( gbfs_products_ref const prod_ref) { return utl::get_or_create(products_ref_to_transport_mode_, prod_ref, [&]() { auto const id = static_cast( kGbfsTransportModeIdOffset + products_refs_.size()); products_refs_.emplace_back(prod_ref); return id; }); } gbfs_products_ref gbfs_routing_data::get_products_ref( nigiri::transport_mode_id_t const id) const { return products_refs_.at( static_cast(id - kGbfsTransportModeIdOffset)); } } // namespace motis::gbfs ================================================ FILE: src/gbfs/update.cc ================================================ #include "motis/gbfs/update.h" #include #include #include #include #include #include #include #include #include #include #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/asio/experimental/awaitable_operators.hpp" #include "boost/asio/experimental/parallel_group.hpp" #include "boost/asio/redirect_error.hpp" #include "boost/asio/steady_timer.hpp" #include "boost/stacktrace.hpp" #include "boost/json.hpp" #include "boost/url/encode.hpp" #include "boost/url/rfc/unreserved_chars.hpp" #include "cista/hash.h" #include "fmt/format.h" #include "utl/enumerate.h" #include "utl/helpers/algorithm.h" #include "utl/overloaded.h" #include "utl/sorted_diff.h" #include "utl/timer.h" #include "utl/to_vec.h" #include "utl/verify.h" #include "motis/config.h" #include "motis/data.h" #include "motis/gbfs/data.h" #include "motis/http_req.h" #include "motis/gbfs/compression.h" #include "motis/gbfs/osr_mapping.h" #include "motis/gbfs/parser.h" #include "motis/gbfs/partition.h" #include "motis/gbfs/routing_data.h" namespace asio = boost::asio; using asio::awaitable; using namespace asio::experimental::awaitable_operators; namespace json = boost::json; namespace motis::gbfs { struct gbfs_file { json::value json_; cista::hash_t hash_{}; std::chrono::system_clock::time_point next_refresh_; }; std::string read_file(std::filesystem::path const& path) { auto is = std::ifstream{path}; auto buf = std::stringstream{}; buf << is.rdbuf(); return buf.str(); } bool needs_refresh(file_info const& fi) { return fi.needs_update(std::chrono::system_clock::now()); } // try to hash only the value of the "data" key to ignore fields like // "last_updated" cista::hash_t hash_gbfs_data(std::string_view const json) { auto const pos = json.find("\"data\""); if (pos == std::string_view::npos) { return cista::hash(json); } auto i = pos + 6; auto const skip_whitespace = [&]() { while (i < json.size() && (json[i] == ' ' || json[i] == '\n' || json[i] == '\r' || json[i] == '\t')) { ++i; } }; skip_whitespace(); if (i >= json.size() || json[i++] != ':') { return cista::hash(json); } skip_whitespace(); if (i >= json.size() || json[i] != '{') { return cista::hash(json); } auto const start = i; auto depth = 1; auto in_string = false; while (++i < json.size()) { if (in_string) { if (json[i] == '"' && json[i - 1] != '\\') { in_string = false; } continue; } switch (json[i]) { case '"': in_string = true; break; case '{': ++depth; break; case '}': if (--depth == 0) { return cista::hash(json.substr(start, i - start + 1)); } } } return cista::hash(json); } std::chrono::system_clock::time_point get_expiry( json::object const& root, std::chrono::seconds const def = std::chrono::seconds{0}, std::map const& default_ttl = {}, std::map const& overwrite_ttl = {}, std::string_view const name = "") { auto const now = std::chrono::system_clock::now(); if (auto const it = overwrite_ttl.find(std::string{name}); it != end(overwrite_ttl)) { return now + std::chrono::seconds{it->second}; } if (root.contains("data")) { auto const& data = root.at("data").as_object(); if (data.contains("ttl")) { auto const ttl = data.at("ttl").to_number(); if (ttl > 0) { return now + std::chrono::seconds{ttl}; } } } if (auto const it = default_ttl.find(std::string{name}); it != end(default_ttl)) { return now + std::chrono::seconds{it->second}; } return now + def; } struct gbfs_update { gbfs_update(config::gbfs const& c, osr::ways const& w, osr::lookup const& l, gbfs_data* d, gbfs_data const* prev_d) : c_{c}, w_{w}, l_{l}, d_{d}, prev_d_{prev_d}, timeout_{c.http_timeout_}, proxy_{c.proxy_.transform([](std::string const& u) { auto const url = boost::urls::url{u}; auto p = proxy{}; p.use_tls_ = url.scheme_id() == boost::urls::scheme::https; p.host_ = url.host(); p.port_ = url.has_port() ? url.port() : (p.use_tls_ ? "443" : "80"); return p; })} {} awaitable run() { auto executor = co_await asio::this_coro::executor; if (prev_d_ == nullptr) { // this is first time gbfs_update is run: initialize feeds from config d_->aggregated_feeds_ = std::make_shared>>(); d_->standalone_feeds_ = std::make_shared>>(); for (auto const& [id, group] : c_.groups_) { d_->groups_.emplace(id, gbfs_group{.id_ = id, .name_ = group.name_.value_or(id), .color_ = group.color_}); } auto awaitables = utl::to_vec(c_.feeds_, [&](auto const& f) { auto const& id = f.first; auto const& feed = f.second; auto const dir = feed.url_.starts_with("http:") || feed.url_.starts_with("https:") ? std::nullopt : std::optional{feed.url_}; return boost::asio::co_spawn( executor, [this, id, feed, dir]() -> awaitable { co_await init_feed(id, feed, dir); }, asio::deferred); }); co_await asio::experimental::make_parallel_group(awaitables) .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable); } else { // update run: copy over data from previous state and update feeds // where necessary d_->aggregated_feeds_ = prev_d_->aggregated_feeds_; d_->standalone_feeds_ = prev_d_->standalone_feeds_; // the set of providers can change if aggregated feeds are used + change. // gbfs_provider_idx_t for existing providers is stable, if a provider is // removed its entry is set to a nullptr. new providers may be added. d_->providers_.resize(prev_d_->providers_.size()); d_->provider_by_id_ = prev_d_->provider_by_id_; d_->provider_rtree_ = prev_d_->provider_rtree_; d_->provider_zone_rtree_ = prev_d_->provider_zone_rtree_; d_->cache_ = prev_d_->cache_; d_->groups_ = prev_d_->groups_; for (auto& group : d_->groups_ | std::views::values) { group.providers_.clear(); } co_await refresh_oauth_tokens(); if (!d_->aggregated_feeds_->empty()) { co_await asio::experimental::make_parallel_group( utl::to_vec(*d_->aggregated_feeds_, [&](auto const& af) { return boost::asio::co_spawn( executor, [this, af = af.get()]() -> awaitable { co_await update_aggregated_feed(*af); }, asio::deferred); })) .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable); } if (!d_->standalone_feeds_->empty()) { co_await asio::experimental::make_parallel_group( utl::to_vec(*d_->standalone_feeds_, [&](auto const& pf) { return boost::asio::co_spawn( executor, [this, pf = pf.get()]() -> awaitable { co_await update_provider_feed(*pf); }, asio::deferred); })) .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable); } } } awaitable init_feed(std::string const& id, config::gbfs::feed const& config, std::optional const& dir) { // initialization of a (standalone or aggregated) feed from the config try { auto const headers = config.headers_.value_or(headers_t{}); auto oauth = std::shared_ptr{}; if (config.oauth_) { oauth = std::make_shared( oauth_state{.settings_ = *config.oauth_, .expires_in_ = config.oauth_->expires_in_.value_or(0)}); } auto const merge_ttl_map = [](std::optional> const& feed_map, std::optional> const& global_map) { auto res = global_map.value_or(std::map{}); if (feed_map) { for (auto const& [k, v] : *feed_map) { res[k] = v; } } return res; }; auto const default_ttl = merge_ttl_map(config.ttl_.value_or(config::gbfs::ttl{}).default_, c_.ttl_.value_or(config::gbfs::ttl{}).default_); auto const overwrite_ttl = merge_ttl_map(config.ttl_.value_or(config::gbfs::ttl{}).overwrite_, c_.ttl_.value_or(config::gbfs::ttl{}).overwrite_); auto discovery = co_await fetch_file("gbfs", config.url_, headers, oauth, dir, default_ttl, overwrite_ttl); auto const& root = discovery.json_.as_object(); if ((root.contains("data") && root.at("data").as_object().contains("datasets")) || root.contains("systems")) { // file is not an individual feed, but a manifest.json / Lamassu file co_return co_await init_aggregated_feed(id, config.url_, headers, std::move(oauth), root, default_ttl, overwrite_ttl); } auto saf = d_->standalone_feeds_ ->emplace_back(std::make_unique(provider_feed{ .id_ = id, .url_ = config.url_, .headers_ = headers, .dir_ = dir, .default_restrictions_ = lookup_default_restrictions("", id), .default_return_constraint_ = lookup_default_return_constraint("", id), .config_group_ = lookup_group("", id), .config_color_ = lookup_color("", id), .oauth_ = std::move(oauth), .default_ttl_ = default_ttl, .overwrite_ttl_ = overwrite_ttl})) .get(); co_return co_await update_provider_feed(*saf, std::move(discovery)); } catch (std::exception const& ex) { std::cerr << "[GBFS] error initializing feed " << id << " (" << config.url_ << "): " << ex.what() << "\n"; } } awaitable update_provider_feed( provider_feed const& pf, std::optional discovery = std::nullopt) { auto& provider = add_provider(pf); // check if exists in old data - if so, reuse existing file infos gbfs_provider const* prev_provider = nullptr; if (prev_d_ != nullptr) { if (auto const it = prev_d_->provider_by_id_.find(pf.id_); it != end(prev_d_->provider_by_id_)) { prev_provider = prev_d_->providers_[it->second].get(); if (prev_provider != nullptr) { provider.file_infos_ = prev_provider->file_infos_; } } } if (!provider.file_infos_) { provider.file_infos_ = std::make_shared(); } co_return co_await process_provider_feed(pf, provider, prev_provider, std::move(discovery)); } gbfs_provider& add_provider(provider_feed const& pf) { auto const init_provider = [&](gbfs_provider& provider, gbfs_provider_idx_t const idx) { provider.id_ = pf.id_; provider.idx_ = idx; provider.default_restrictions_ = pf.default_restrictions_; provider.default_return_constraint_ = pf.default_return_constraint_; provider.color_ = pf.config_color_; if (pf.config_group_) { provider.group_id_ = *pf.config_group_; } }; if (auto it = d_->provider_by_id_.find(pf.id_); it != end(d_->provider_by_id_)) { // existing provider, keep idx auto const idx = it->second; assert(d_->providers_.at(idx) == nullptr); d_->providers_[idx] = std::make_unique(); auto& provider = *d_->providers_[idx].get(); init_provider(provider, idx); return provider; } else { // new provider auto const idx = gbfs_provider_idx_t{d_->providers_.size()}; auto& provider = *d_->providers_.emplace_back(std::make_unique()).get(); d_->provider_by_id_[pf.id_] = idx; init_provider(provider, idx); return provider; } } awaitable process_provider_feed( provider_feed const& pf, gbfs_provider& provider, gbfs_provider const* prev_provider, std::optional discovery = std::nullopt) { auto& file_infos = provider.file_infos_; auto data_changed = false; auto geofencing_updated = false; try { if (!discovery && needs_refresh(provider.file_infos_->urls_fi_)) { discovery = co_await fetch_file("gbfs", pf.url_, pf.headers_, pf.oauth_, pf.dir_, pf.default_ttl_, pf.overwrite_ttl_); } if (discovery) { file_infos->urls_ = parse_discovery(discovery->json_); file_infos->urls_fi_.expiry_ = discovery->next_refresh_; file_infos->urls_fi_.hash_ = discovery->hash_; } auto const update = [&](std::string_view const name, file_info& fi, auto const& fn, bool const force = false) -> awaitable { if (!file_infos->urls_.contains(name)) { co_return false; } if (force || needs_refresh(fi)) { auto file = co_await fetch_file(name, file_infos->urls_.at(name), pf.headers_, pf.oauth_, pf.dir_, pf.default_ttl_, pf.overwrite_ttl_); auto const hash_changed = file.hash_ != fi.hash_; auto j_root = file.json_.as_object(); fi.expiry_ = file.next_refresh_; fi.hash_ = file.hash_; fn(provider, file.json_); co_return hash_changed; } co_return false; }; auto const sys_info_updated = co_await update( "system_information", file_infos->system_information_fi_, load_system_information); if (!sys_info_updated && prev_provider != nullptr) { provider.sys_info_ = prev_provider->sys_info_; } auto const vehicle_types_updated = co_await update( "vehicle_types", file_infos->vehicle_types_fi_, load_vehicle_types); if (!vehicle_types_updated && prev_provider != nullptr) { provider.vehicle_types_ = prev_provider->vehicle_types_; provider.vehicle_types_map_ = prev_provider->vehicle_types_map_; provider.temp_vehicle_types_ = prev_provider->temp_vehicle_types_; } auto const stations_updated = co_await update( "station_information", file_infos->station_information_fi_, load_station_information, vehicle_types_updated); if ((!stations_updated && !vehicle_types_updated) && prev_provider != nullptr) { provider.stations_ = prev_provider->stations_; } auto const station_status_updated = co_await update( "station_status", file_infos->station_status_fi_, load_station_status, stations_updated || vehicle_types_updated); auto const vehicle_status_updated = co_await update("vehicle_status", file_infos->vehicle_status_fi_, load_vehicle_status, vehicle_types_updated) // 3.x || co_await update("free_bike_status", file_infos->vehicle_status_fi_, load_vehicle_status, vehicle_types_updated); // 1.x / 2.x if ((!vehicle_status_updated && !vehicle_types_updated) && prev_provider != nullptr) { provider.vehicle_status_ = prev_provider->vehicle_status_; } geofencing_updated = co_await update("geofencing_zones", file_infos->geofencing_zones_fi_, load_geofencing_zones, vehicle_types_updated); if ((!geofencing_updated && !vehicle_types_updated) && prev_provider != nullptr) { provider.geofencing_zones_ = prev_provider->geofencing_zones_; } if (prev_provider != nullptr) { provider.has_vehicles_to_rent_ = prev_provider->has_vehicles_to_rent_; } if (!provider.color_.has_value() && !provider.sys_info_.color_.empty()) { provider.color_ = provider.sys_info_.color_; } auto group_name = std::optional{}; if (provider.group_id_.empty()) { auto generated_id = provider.sys_info_.name_; std::erase(generated_id, ','); provider.group_id_ = generated_id; group_name = provider.sys_info_.name_; } if (auto it = d_->groups_.find(provider.group_id_); it == end(d_->groups_)) { d_->groups_.emplace( provider.group_id_, gbfs_group{.id_ = provider.group_id_, .name_ = group_name.value_or(provider.group_id_), .color_ = {}, .providers_ = {provider.idx_}}); } else { it->second.providers_.push_back(provider.idx_); } if (stations_updated || vehicle_status_updated) { for (auto const& st : provider.stations_ | std::views::values) { provider.bbox_.extend(st.info_.pos_); } for (auto const& vs : provider.vehicle_status_) { provider.bbox_.extend(vs.pos_); } } else if (prev_provider != nullptr) { provider.bbox_ = prev_provider->bbox_; } data_changed = vehicle_types_updated || stations_updated || station_status_updated || vehicle_status_updated || geofencing_updated; } catch (std::exception const& ex) { std::cerr << "[GBFS] error processing feed " << pf.id_ << " (" << pf.url_ << "): " << ex.what() << "\n"; if (!std::string_view{ex.what()}.starts_with("HTTP ")) { if (auto const trace = boost::stacktrace::stacktrace::from_current_exception(); trace) { std::cerr << trace << std::endl; } } // keep previous data if (prev_provider != nullptr) { provider.sys_info_ = prev_provider->sys_info_; provider.vehicle_types_ = prev_provider->vehicle_types_; provider.vehicle_types_map_ = prev_provider->vehicle_types_map_; provider.temp_vehicle_types_ = prev_provider->temp_vehicle_types_; provider.stations_ = prev_provider->stations_; provider.vehicle_status_ = prev_provider->vehicle_status_; provider.geofencing_zones_ = prev_provider->geofencing_zones_; provider.has_vehicles_to_rent_ = prev_provider->has_vehicles_to_rent_; provider.bbox_ = prev_provider->bbox_; } } if (data_changed) { try { partition_provider(provider); provider.has_vehicles_to_rent_ = utl::any_of( provider.products_, [](auto const& prod) { return prod.has_vehicles_to_rent_; }); update_rtree(provider, prev_provider, geofencing_updated); d_->cache_.try_add_or_update(provider.idx_, [&]() { return compute_provider_routing_data(w_, l_, provider); }); } catch (std::exception const& ex) { std::cerr << "[GBFS] error updating provider " << pf.id_ << ": " << ex.what() << "\n"; } } else if (prev_provider != nullptr) { // data not changed, copy previously computed products provider.products_ = prev_provider->products_; provider.has_vehicles_to_rent_ = prev_provider->has_vehicles_to_rent_; } } void partition_provider(gbfs_provider& provider) { if (provider.vehicle_types_.empty()) { auto& prod = provider.products_.emplace_back(); prod.idx_ = gbfs_products_idx_t{0}; prod.has_vehicles_to_rent_ = utl::any_of(provider.stations_, [](auto const& st) { return st.second.status_.is_renting_ && st.second.status_.num_vehicles_available_ > 0; }) || utl::any_of(provider.vehicle_status_, [](auto const& vs) { return !vs.is_disabled_ && !vs.is_reserved_; }); } else { auto part = partition{vehicle_type_idx_t{provider.vehicle_types_.size()}}; // refine by form factor + propulsion type auto by_form_factor = hash_map, std::vector>{}; for (auto const& vt : provider.vehicle_types_) { by_form_factor[std::pair{vt.form_factor_, vt.propulsion_type_}] .push_back(vt.idx_); } for (auto const& [_, vt_indices] : by_form_factor) { part.refine(vt_indices); } // refine by return constraints auto by_return_constraint = hash_map>{}; for (auto const& vt : provider.vehicle_types_) { by_return_constraint[vt.return_constraint_].push_back(vt.idx_); } for (auto const& [_, vt_indices] : by_return_constraint) { part.refine(vt_indices); } // refine by known vs. guessed return constraints auto known_return_constraints = std::vector{}; auto guessed_return_constraints = std::vector{}; for (auto const& vt : provider.vehicle_types_) { if (vt.known_return_constraint_) { known_return_constraints.push_back(vt.idx_); } else { guessed_return_constraints.push_back(vt.idx_); } } part.refine(known_return_constraints); part.refine(guessed_return_constraints); // refine by return stations // TODO: only do this if the station is not in a zone where vehicles // can be returned anywhere auto vts = std::vector{}; for (auto const& [id, st] : provider.stations_) { if (!st.status_.vehicle_docks_available_.empty()) { vts.clear(); for (auto const& [vt, num] : st.status_.vehicle_docks_available_) { vts.push_back(vt); } part.refine(vts); } } // refine by geofencing zones for (auto const& z : provider.geofencing_zones_.zones_) { for (auto const& r : z.rules_) { part.refine(r.vehicle_type_idxs_); } } for (auto const& set : part.get_sets()) { auto const prod_idx = gbfs_products_idx_t{provider.products_.size()}; auto& prod = provider.products_.emplace_back(); prod.idx_ = prod_idx; prod.vehicle_types_ = set; auto const first_vt = provider.vehicle_types_.at(prod.vehicle_types_.front()); prod.form_factor_ = first_vt.form_factor_; prod.propulsion_type_ = first_vt.propulsion_type_; prod.return_constraint_ = first_vt.return_constraint_; prod.known_return_constraint_ = first_vt.known_return_constraint_; prod.has_vehicles_to_rent_ = utl::any_of(provider.stations_, [&](auto const& st) { return st.second.status_.is_renting_ && st.second.status_.num_vehicles_available_ > 0; }) || utl::any_of(provider.vehicle_status_, [&](auto const& vs) { return !vs.is_disabled_ && !vs.is_reserved_ && prod.includes_vehicle_type(vs.vehicle_type_idx_); }); } } } void update_rtree(gbfs_provider const& provider, gbfs_provider const* prev_provider, bool const zones_changed) { auto added_stations = 0U; auto added_vehicles = 0U; auto removed_stations = 0U; auto removed_vehicles = 0U; auto moved_stations = 0U; auto moved_vehicles = 0U; if (prev_provider != nullptr) { using ST = std::pair; utl::sorted_diff( prev_provider->stations_, provider.stations_, [](ST const& a, ST const& b) { return a.first < b.first; }, [](ST const& a, ST const& b) { return a.second.info_.pos_ == b.second.info_.pos_; }, utl::overloaded{ [&](utl::op const o, ST const& s) { if (o == utl::op::kAdd) { d_->provider_rtree_.add(s.second.info_.pos_, provider.idx_); ++added_stations; } else { // del d_->provider_rtree_.remove(s.second.info_.pos_, provider.idx_); ++removed_stations; } }, [&](ST const& a, ST const& b) { d_->provider_rtree_.remove(a.second.info_.pos_, provider.idx_); d_->provider_rtree_.add(b.second.info_.pos_, provider.idx_); ++moved_stations; }}); utl::sorted_diff( prev_provider->vehicle_status_, provider.vehicle_status_, [](vehicle_status const& a, vehicle_status const& b) { return a.id_ < b.id_; }, [](vehicle_status const& a, vehicle_status const& b) { return a.pos_ == b.pos_; }, utl::overloaded{ [&](utl::op const o, vehicle_status const& v) { if (o == utl::op::kAdd) { d_->provider_rtree_.add(v.pos_, provider.idx_); ++added_vehicles; } else { // del d_->provider_rtree_.remove(v.pos_, provider.idx_); ++removed_vehicles; } }, [&](vehicle_status const& a, vehicle_status const& b) { d_->provider_rtree_.remove(a.pos_, provider.idx_); d_->provider_rtree_.add(b.pos_, provider.idx_); ++moved_vehicles; }}); if (zones_changed) { for (auto const& zone : prev_provider->geofencing_zones_.zones_) { d_->provider_zone_rtree_.remove(zone.bounding_box(), provider.idx_); } for (auto const& zone : provider.geofencing_zones_.zones_) { if (zone.allows_rental_operation()) { d_->provider_zone_rtree_.add(zone.bounding_box(), provider.idx_); } } } } else { for (auto const& station : provider.stations_) { d_->provider_rtree_.add(station.second.info_.pos_, provider.idx_); ++added_stations; } for (auto const& vehicle : provider.vehicle_status_) { if (vehicle.station_id_.empty()) { d_->provider_rtree_.add(vehicle.pos_, provider.idx_); ++added_vehicles; } } for (auto const& zone : provider.geofencing_zones_.zones_) { if (zone.allows_rental_operation()) { d_->provider_zone_rtree_.add(zone.bounding_box(), provider.idx_); } } } } awaitable init_aggregated_feed( std::string const& prefix, std::string const& url, headers_t const& headers, std::shared_ptr&& oauth, boost::json::object const& root, std::map const& default_ttl = {}, std::map const& overwrite_ttl = {}) { auto af = d_->aggregated_feeds_ ->emplace_back(std::make_unique(aggregated_feed{ .id_ = prefix, .url_ = url, .headers_ = headers, .expiry_ = get_expiry(root, std::chrono::hours{1}, default_ttl, overwrite_ttl, "manifest"), .oauth_ = std::move(oauth), .default_ttl_ = default_ttl, .overwrite_ttl_ = overwrite_ttl})) .get(); co_return co_await process_aggregated_feed(*af, root); } awaitable update_aggregated_feed(aggregated_feed& af) { if (af.needs_update()) { auto const file = co_await fetch_file("manifest", af.url_, af.headers_, af.oauth_, std::nullopt, af.default_ttl_, af.overwrite_ttl_); co_await process_aggregated_feed(af, file.json_.as_object()); } else { co_await update_aggregated_feed_provider_feeds(af); } } awaitable process_aggregated_feed(aggregated_feed& af, boost::json::object const& root) { auto feeds = std::vector{}; if (root.contains("data") && root.at("data").as_object().contains("datasets")) { // GBFS 3.x manifest.json for (auto const& dataset : root.at("data").at("datasets").as_array()) { auto const system_id = static_cast(dataset.at("system_id").as_string()); auto const combined_id = fmt::format("{}:{}", af.id_, system_id); auto const& versions = dataset.at("versions").as_array(); if (versions.empty()) { continue; } // versions array must be sorted by increasing version number auto const& latest_version = versions.back().as_object(); feeds.emplace_back(provider_feed{ .id_ = combined_id, .url_ = static_cast(latest_version.at("url").as_string()), .headers_ = af.headers_, .default_restrictions_ = lookup_default_restrictions(af.id_, combined_id), .default_return_constraint_ = lookup_default_return_constraint(af.id_, combined_id), .config_group_ = lookup_group(af.id_, system_id), .config_color_ = lookup_color(af.id_, system_id), .oauth_ = af.oauth_, .default_ttl_ = af.default_ttl_, .overwrite_ttl_ = af.overwrite_ttl_}); } } else if (root.contains("systems")) { // Lamassu 2.3 format for (auto const& system : root.at("systems").as_array()) { auto const system_id = static_cast(system.at("id").as_string()); auto const combined_id = fmt::format("{}:{}", af.id_, system_id); feeds.emplace_back(provider_feed{ .id_ = combined_id, .url_ = static_cast(system.at("url").as_string()), .headers_ = af.headers_, .default_restrictions_ = lookup_default_restrictions(af.id_, combined_id), .default_return_constraint_ = lookup_default_return_constraint(af.id_, combined_id), .config_group_ = lookup_group(af.id_, system_id), .config_color_ = lookup_color(af.id_, system_id), .oauth_ = af.oauth_, .default_ttl_ = af.default_ttl_, .overwrite_ttl_ = af.overwrite_ttl_}); } } af.feeds_ = std::move(feeds); co_await update_aggregated_feed_provider_feeds(af); } awaitable update_aggregated_feed_provider_feeds(aggregated_feed& af) { auto executor = co_await asio::this_coro::executor; co_await asio::experimental::make_parallel_group( utl::to_vec(af.feeds_, [&](auto const& pf) { return boost::asio::co_spawn( executor, [this, pf = &pf]() -> awaitable { co_await update_provider_feed(*pf); }, asio::deferred); })) .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable); } awaitable fetch_file( std::string_view const name, std::string_view const url, headers_t const& base_headers, std::shared_ptr const& oauth, std::optional const& dir = std::nullopt, std::map const& default_ttl = {}, std::map const& overwrite_ttl = {}) { auto content = std::string{}; if (dir.has_value()) { content = read_file(*dir / fmt::format("{}.json", name)); } else { auto headers = base_headers; co_await get_oauth_token(oauth, headers); auto const res = co_await http_GET(boost::urls::url{url}, std::move(headers), timeout_, proxy_); content = get_http_body(res); if (res.result_int() != 200) { throw std::runtime_error( fmt::format("HTTP {} fetching {}", res.result_int(), url)); } } auto j = json::parse(content); auto j_root = j.as_object(); auto const next_refresh = get_expiry(j_root, std::chrono::seconds{0}, default_ttl, overwrite_ttl, name); co_return gbfs_file{.json_ = std::move(j), .hash_ = hash_gbfs_data(content), .next_refresh_ = next_refresh}; } awaitable get_oauth_token(std::shared_ptr const& oauth, headers_t& headers, std::chrono::seconds remaining_time_required = std::chrono::seconds{120}) { if (oauth == nullptr || oauth->settings_.token_url_.empty()) { co_return; } co_await refresh_oauth_token(oauth, remaining_time_required); headers["Authorization"] = fmt::format("Bearer {}", oauth->access_token_); } awaitable refresh_oauth_token( std::shared_ptr const& oauth, std::chrono::seconds remaining_time_required) { if (oauth == nullptr || oauth->settings_.token_url_.empty()) { co_return; } if (!oauth->access_token_.empty() && oauth->expiry_.has_value() && (*oauth->expiry_ - std::chrono::system_clock::now()) > remaining_time_required) { // token still valid co_return; } try { auto const opt = boost::urls::encoding_opts(true); auto const body = fmt::format( "grant_type=client_credentials&client_id={}&client_secret={}", boost::urls::encode(oauth->settings_.client_id_, boost::urls::unreserved_chars, opt), boost::urls::encode(oauth->settings_.client_secret_, boost::urls::unreserved_chars, opt)); auto oauth_headers = oauth->settings_.headers_.value_or(headers_t{}); oauth_headers["Content-Type"] = "application/x-www-form-urlencoded"; auto const res = co_await http_POST(boost::urls::url{oauth->settings_.token_url_}, std::move(oauth_headers), body, timeout_); auto const res_body = get_http_body(res); auto const res_json = json::parse(res_body); auto const& j = res_json.as_object(); if (res.result_int() != 200) { std::cerr << "[GBFS] oauth token request failed: "; if (j.contains("error")) { std::cerr << j.at("error").as_string(); } else { std::cerr << "HTTP " << res.result_int(); } if (j.contains("error_description")) { std::cerr << " (" << j.at("error_description").as_string() << ")"; } if (j.contains("error_uri")) { std::cerr << " (" << j.at("error_uri").as_string() << ")"; } std::cerr << " (token url: " << oauth->settings_.token_url_ << ")" << std::endl; throw std::runtime_error("oauth token request failed"); } auto const token_type = j.at("token_type").as_string(); utl::verify(token_type == "Bearer", "unsupported oauth token type \"{}\"", token_type); oauth->access_token_ = static_cast(j.at("access_token").as_string()); oauth->expires_in_ = oauth->settings_.expires_in_.value_or(60 * 60 * 24); if (j.contains("expires_in")) { oauth->expires_in_ = std::min(oauth->expires_in_, j.at("expires_in").to_number()); } oauth->expiry_ = std::chrono::system_clock::now() + std::chrono::seconds{oauth->expires_in_}; } catch (std::runtime_error const& e) { std::cerr << "[GBFS] oauth token request error: " << e.what() << std::endl; throw; } } awaitable refresh_oauth_tokens() { auto states = std::set>{}; for (auto const& af : *d_->aggregated_feeds_) { if (af->oauth_ != nullptr) { states.insert(af->oauth_); } } for (auto const& pf : *d_->standalone_feeds_) { if (pf->oauth_ != nullptr) { states.insert(pf->oauth_); } } if (states.empty()) { // this is necessary, because calling async_wait on an empty group // causes everything to break co_return; } auto executor = co_await asio::this_coro::executor; co_await asio::experimental::make_parallel_group( utl::to_vec(states, [&](auto const& state) { return boost::asio::co_spawn( executor, [this, state]() -> awaitable { co_await refresh_oauth_token( state, std::chrono::seconds{state->expires_in_ / 2}); }, asio::deferred); })) .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable); } geofencing_restrictions lookup_default_restrictions(std::string const& prefix, std::string const& id) { auto const convert = [&](config::gbfs::restrictions const& r) { return geofencing_restrictions{ .ride_start_allowed_ = r.ride_start_allowed_, .ride_end_allowed_ = r.ride_end_allowed_, .ride_through_allowed_ = r.ride_through_allowed_, .station_parking_ = r.station_parking_}; }; if (auto const it = c_.default_restrictions_.find(id); it != end(c_.default_restrictions_)) { return convert(it->second); } else if (auto const prefix_it = c_.default_restrictions_.find(prefix); prefix_it != end(c_.default_restrictions_)) { return convert(prefix_it->second); } else { return {}; } } std::optional lookup_default_return_constraint( std::string const& prefix, std::string const& id) { auto const convert = [&](config::gbfs::restrictions const& r) { return r.return_constraint_.has_value() ? parse_return_constraint(r.return_constraint_.value()) : std::nullopt; }; if (auto const it = c_.default_restrictions_.find(id); it != end(c_.default_restrictions_)) { return convert(it->second); } else if (auto const prefix_it = c_.default_restrictions_.find(prefix); prefix_it != end(c_.default_restrictions_)) { return convert(prefix_it->second); } else { return {}; } } template std::optional lookup_mapping(std::string const& af_id, std::string const& system_id, Getter getter) { auto const& af_config = c_.feeds_.at(af_id.empty() ? system_id : af_id); auto const& opt = getter(af_config); if (opt.has_value()) { return std::visit( utl::overloaded{ [&](std::string const& s) -> std::optional { return std::optional{s}; }, [&](std::map const& m) -> std::optional { if (auto const it = m.find(system_id); it != end(m)) { return std::optional{it->second}; } return {}; }}, *opt); } return {}; } std::optional lookup_group(std::string const& af_id, std::string const& system_id) { return lookup_mapping(af_id, system_id, [](auto const& cfg) { return cfg.group_; }); } std::optional lookup_color(std::string const& af_id, std::string const& system_id) { return lookup_mapping(af_id, system_id, [](auto const& cfg) { return cfg.color_; }); } config::gbfs const& c_; osr::ways const& w_; osr::lookup const& l_; gbfs_data* d_; gbfs_data const* prev_d_; std::chrono::seconds timeout_; std::optional proxy_; }; awaitable update(config const& c, osr::ways const& w, osr::lookup const& l, std::shared_ptr& data_ptr) { auto const t = utl::scoped_timer{"gbfs::update"}; if (!c.gbfs_.has_value()) { co_return; } auto const prev_d = data_ptr; auto const d = std::make_shared(c.gbfs_->cache_size_); auto update = gbfs_update{*c.gbfs_, w, l, d.get(), prev_d.get()}; try { co_await update.run(); } catch (std::exception const& e) { std::cerr << "[GBFS] update error: " << e.what() << std::endl; if (auto const trace = boost::stacktrace::stacktrace::from_current_exception(); trace) { std::cerr << trace << std::endl; } } data_ptr = d; } void run_gbfs_update(boost::asio::io_context& ioc, config const& c, osr::ways const& w, osr::lookup const& l, std::shared_ptr& data_ptr) { boost::asio::co_spawn( ioc, [&]() -> awaitable { auto executor = co_await asio::this_coro::executor; auto timer = asio::steady_timer{executor}; auto ec = boost::system::error_code{}; auto cc = c; while (true) { // Remember when we started so we can schedule the next update. auto const start = std::chrono::steady_clock::now(); co_await update(cc, w, l, data_ptr); // Schedule next update. timer.expires_at(start + std::chrono::seconds{cc.gbfs_->update_interval_}); co_await timer.async_wait( asio::redirect_error(asio::use_awaitable, ec)); if (ec == asio::error::operation_aborted) { co_return; } } }, boost::asio::detached); } } // namespace motis::gbfs ================================================ FILE: src/get_stops_with_traffic.cc ================================================ #include "motis/get_stops_with_traffic.h" #include "osr/location.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/timetable.h" namespace n = nigiri; namespace motis { std::vector get_stops_with_traffic( n::timetable const& tt, n::rt_timetable const* rtt, point_rtree const& rtree, osr::location const& pos, double const distance, n::location_idx_t const not_equal_to) { auto ret = std::vector{}; rtree.in_radius(pos.pos_, distance, [&](n::location_idx_t const l) { if (tt.location_routes_[l].empty() && (rtt == nullptr || rtt->location_rt_transports_[l].empty())) { return; } if (l == not_equal_to) { return; } ret.emplace_back(l); }); return ret; } } // namespace motis ================================================ FILE: src/hashes.cc ================================================ #include "motis/hashes.h" #include #include #include "boost/json.hpp" #include "cista/mmap.h" namespace fs = std::filesystem; namespace motis { std::string to_str(meta_t const& h) { return boost::json::serialize(boost::json::value_from(h)); } meta_t read_hashes(fs::path const& data_path, std::string const& name) { auto const p = (data_path / "meta" / (name + ".json")); if (!exists(p)) { return {}; } auto const mmap = cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}; return boost::json::value_to(boost::json::parse(mmap.view())); } void write_hashes(fs::path const& data_path, std::string const& name, meta_t const& h) { auto const p = (data_path / "meta" / (name + ".json")); std::ofstream{p} << to_str(h); } } // namespace motis ================================================ FILE: src/http_req.cc ================================================ #include "motis/http_req.h" #include "boost/asio/awaitable.hpp" #include "boost/asio/co_spawn.hpp" #include "boost/asio/io_context.hpp" #include "boost/asio/ssl.hpp" #include "boost/beast/core.hpp" #include "boost/beast/http.hpp" #include "boost/beast/http/dynamic_body.hpp" #include "boost/beast/ssl/ssl_stream.hpp" #include "boost/beast/version.hpp" #include "boost/iostreams/copy.hpp" #include "boost/iostreams/filter/gzip.hpp" #include "boost/iostreams/filtering_stream.hpp" #include "boost/iostreams/filtering_streambuf.hpp" #include "boost/url/url.hpp" #include "utl/verify.h" namespace motis { namespace beast = boost::beast; namespace http = beast::http; namespace asio = boost::asio; namespace ssl = asio::ssl; template asio::awaitable req( Stream&&, boost::urls::url const&, std::map const&, std::optional const& body = std::nullopt); asio::awaitable req_no_tls( boost::urls::url const& url, std::map const& headers, std::optional const& body, std::chrono::seconds const timeout, std::optional const& proxy) { auto executor = co_await asio::this_coro::executor; auto resolver = asio::ip::tcp::resolver{executor}; auto stream = beast::tcp_stream{executor}; auto const host = proxy ? proxy->host_ : url.host(); auto const port = proxy ? proxy->port_ : std::string{url.has_port() ? url.port() : "80"}; auto const results = co_await resolver.async_resolve(host, port); stream.expires_after(timeout); co_await stream.async_connect(results); co_return co_await req(std::move(stream), url, headers, body); } asio::awaitable req_tls( boost::urls::url const& url, std::map const& headers, std::optional const& body, std::chrono::seconds const timeout, std::optional const& proxy) { auto ssl_ctx = ssl::context{ssl::context::tlsv12_client}; ssl_ctx.set_default_verify_paths(); ssl_ctx.set_verify_mode(ssl::verify_none); ssl_ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2 | ssl::context::no_sslv3 | ssl::context::single_dh_use); auto executor = co_await asio::this_coro::executor; auto resolver = asio::ip::tcp::resolver{executor}; auto stream = ssl::stream{executor, ssl_ctx}; auto const host = proxy ? proxy->host_ : url.host(); auto const port = proxy ? proxy->port_ : std::string{url.has_port() ? url.port() : "443"}; if (!SSL_set_tlsext_host_name(stream.native_handle(), const_cast(host.c_str()))) { throw boost::system::system_error{ {static_cast(::ERR_get_error()), asio::error::get_ssl_category()}}; } auto const results = co_await resolver.async_resolve(host, port); stream.next_layer().expires_after(timeout); co_await beast::get_lowest_layer(stream).async_connect(results); co_await stream.async_handshake(ssl::stream_base::client); co_return co_await req(std::move(stream), url, headers, body); } template asio::awaitable req( Stream&& stream, boost::urls::url const& url, std::map const& headers, std::optional const& body) { auto req = http::request{ body ? http::verb::post : http::verb::get, url.encoded_target(), 11}; req.set(http::field::host, url.host()); req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); req.set(http::field::accept_encoding, "gzip"); for (auto const& [k, v] : headers) { req.set(k, v); } if (body) { req.body() = *body; req.prepare_payload(); } co_await http::async_write(stream, req); auto p = http::response_parser{}; p.eager(true); p.body_limit(kBodySizeLimit); auto buffer = beast::flat_buffer{}; co_await http::async_read(stream, buffer, p); auto ec = beast::error_code{}; beast::get_lowest_layer(stream).socket().shutdown( asio::ip::tcp::socket::shutdown_both, ec); co_return p.release(); } asio::awaitable> http_GET( boost::urls::url url, std::map const& headers, std::chrono::seconds const timeout, std::optional const& proxy) { auto n_redirects = 0U; auto next_url = url; while (n_redirects < 3U) { auto const use_tls = proxy.has_value() ? proxy->use_tls_ : next_url.scheme_id() == boost::urls::scheme::https; auto const res = co_await ( use_tls ? req_tls(next_url, headers, std::nullopt, timeout, proxy) : req_no_tls(next_url, headers, std::nullopt, timeout, proxy)); auto const code = res.base().result_int(); if (code >= 300 && code < 400) { next_url = boost::urls::url{res.base()["Location"]}; ++n_redirects; continue; } else { co_return res; } } throw utl::fail(R"(too many redirects: "{}", latest="{}")", fmt::streamed(url), fmt::streamed(next_url)); } asio::awaitable> http_POST( boost::urls::url url, std::map const& headers, std::string const& body, std::chrono::seconds timeout, std::optional const& proxy) { auto n_redirects = 0U; auto next_url = url; while (n_redirects < 3U) { auto const use_tls = proxy.has_value() ? proxy->use_tls_ : next_url.scheme_id() == boost::urls::scheme::https; auto const res = co_await ( use_tls ? req_tls(next_url, headers, body, timeout, proxy) : req_no_tls(next_url, headers, body, timeout, proxy)); auto const code = res.base().result_int(); if (code >= 300 && code < 400) { next_url = boost::urls::url{res.base()["Location"]}; ++n_redirects; continue; } else { co_return res; } } throw utl::fail(R"(too many redirects: "{}", latest="{}")", fmt::streamed(url), fmt::streamed(next_url)); } std::string get_http_body(http_response const& res) { auto body = beast::buffers_to_string(res.body().data()); if (res[http::field::content_encoding] == "gzip") { auto const src = boost::iostreams::array_source{body.data(), body.size()}; auto is = boost::iostreams::filtering_istream{}; auto os = std::stringstream{}; is.push(boost::iostreams::gzip_decompressor{}); is.push(src); boost::iostreams::copy(is, os); body = os.str(); } return body; } } // namespace motis ================================================ FILE: src/import.cc ================================================ #include "motis/import.h" #include #include #include #include #include #include "fmt/ranges.h" #include "cista/free_self_allocated.h" #include "cista/io.h" #include "adr/area_database.h" #include "utl/erase_if.h" #include "utl/read_file.h" #include "utl/to_vec.h" #include "tiles/db/clear_database.h" #include "tiles/db/feature_inserter_mt.h" #include "tiles/db/feature_pack.h" #include "tiles/db/pack_file.h" #include "tiles/db/prepare_tiles.h" #include "tiles/db/tile_database.h" #include "tiles/osm/load_coastlines.h" #include "tiles/osm/load_osm.h" #include "nigiri/loader/assistance.h" #include "nigiri/loader/load.h" #include "nigiri/loader/loader_interface.h" #include "nigiri/clasz.h" #include "nigiri/common/parse_date.h" #include "nigiri/routing/tb/preprocess.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/shapes_storage.h" #include "nigiri/timetable.h" #include "nigiri/timetable_metrics.h" #include "osr/extract/extract.h" #include "osr/lookup.h" #include "osr/ways.h" #include "adr/adr.h" #include "adr/formatter.h" #include "adr/reverse.h" #include "adr/typeahead.h" #include "motis/adr_extend_tt.h" #include "motis/clog_redirect.h" #include "motis/compute_footpaths.h" #include "motis/data.h" #include "motis/hashes.h" #include "motis/route_shapes.h" #include "motis/tag_lookup.h" #include "motis/tt_location_rtree.h" namespace fs = std::filesystem; namespace n = nigiri; namespace nl = nigiri::loader; using namespace std::string_literals; using std::chrono_literals::operator""min; using std::chrono_literals::operator""h; namespace motis { struct task { friend std::ostream& operator<<(std::ostream& out, task const& t) { out << t.name_ << " "; if (!t.should_run_) { out << "disabled"; } else if (t.done_) { out << "done"; } else if (utl::all_of(t.dependencies_, [](task const* t) { return t->done_; })) { out << "ready"; } else { out << "waiting for {"; auto first = true; for (auto const& dep : t.dependencies_) { if (dep->done_) { continue; } if (!first) { out << ", "; } first = false; out << dep->name_; } out << "}"; } return out; } bool ready_for_load(fs::path const& data_path) { auto const existing = read_hashes(data_path, name_); if (existing != hashes_) { std::cout << name_ << "\n" << " existing: " << to_str(existing) << "\n" << " current: " << to_str(hashes_) << "\n"; } return existing == hashes_; } void run(fs::path const& data_path) { auto const pt = utl::activate_progress_tracker(name_); auto const redirect = clog_redirect{ (data_path / "logs" / (name_ + ".txt")).generic_string().c_str()}; run_(); write_hashes(data_path, name_, hashes_); pt->out_ = 100; pt->status("FINISHED"); done_ = true; } std::string name_; std::vector dependencies_; bool should_run_; std::function run_; meta_t hashes_; bool done_{false}; utl::progress_tracker_ptr pt_{}; }; } // namespace motis template <> struct fmt::formatter : fmt::ostream_formatter {}; namespace motis { cista::hash_t hash_file(fs::path const& p) { auto const str = p.generic_string(); if (str.starts_with("\nfunction") || str.starts_with("\n#")) { return cista::hash(str); } else if (fs::is_directory(p)) { auto h = cista::BASE_HASH; auto entries = std::vector>{}; for (auto const& entry : fs::recursive_directory_iterator{p}) { auto ec = std::error_code{}; entries.emplace_back(fs::relative(entry.path(), p, ec).generic_string(), entry.is_regular_file(ec) ? entry.file_size(ec) : 0U, fs::last_write_time(entry.path(), ec)); } utl::sort(entries); for (auto const& [rel, size, modified_ts] : entries) { h = cista::hash_combine(h, cista::hash(rel), size, modified_ts.time_since_epoch().count()); } return h; } else { auto const mmap = cista::mmap{str.c_str(), cista::mmap::protection::READ}; return cista::hash_combine( cista::hash(mmap.view().substr( 0U, std::min(mmap.size(), static_cast(50U * 1024U * 1024U)))), mmap.size()); } } void import(config const& c, fs::path const& data_path, std::optional> const& task_filter) { c.verify_input_files_exist(); auto ec = std::error_code{}; fs::create_directories(data_path / "logs", ec); fs::create_directories(data_path / "meta", ec); { auto cfg = std::ofstream{(data_path / "config.yml").generic_string()}; cfg.exceptions(std::ios_base::badbit | std::ios_base::eofbit); cfg << c << "\n"; cfg.close(); } clog_redirect::set_enabled(true); auto tt_hash = std::pair{"timetable"s, cista::BASE_HASH}; if (c.timetable_.has_value()) { auto& h = tt_hash.second; auto const& t = *c.timetable_; for (auto const& [_, d] : t.datasets_) { h = cista::build_seeded_hash( h, c.osr_footpath_, hash_file(d.path_), d.default_bikes_allowed_, d.default_cars_allowed_, d.clasz_bikes_allowed_, d.clasz_cars_allowed_, d.default_timezone_, d.extend_calendar_); if (d.script_.has_value()) { h = cista::build_seeded_hash(h, hash_file(*d.script_)); } } h = cista::build_seeded_hash( h, t.first_day_, t.num_days_, t.with_shapes_, t.adjust_footpaths_, t.merge_dupes_intra_src_, t.merge_dupes_inter_src_, t.link_stop_distance_, t.update_interval_, t.incremental_rt_update_, t.max_footpath_length_, t.default_timezone_, t.assistance_times_); } auto osm_hash = std::pair{"osm"s, cista::BASE_HASH}; if (c.osm_.has_value()) { osm_hash.second = hash_file(*c.osm_); } auto const elevation_dir = c.get_street_routing() .and_then([](config::street_routing const& sr) { return sr.elevation_data_dir_; }) .value_or(fs::path{}); auto elevation_dir_hash = std::pair{"elevation_dir"s, cista::BASE_HASH}; if (!elevation_dir.empty() && fs::exists(elevation_dir)) { auto files = std::vector{}; for (auto const& f : fs::recursive_directory_iterator(elevation_dir)) { if (f.is_regular_file()) { files.emplace_back(f.path().relative_path().string()); } } std::ranges::sort(files); auto& h = elevation_dir_hash.second; for (auto const& f : files) { h = cista::build_seeded_hash(h, f); } } auto tiles_hash = std::pair{"tiles_profile", cista::BASE_HASH}; if (c.tiles_.has_value()) { auto& h = tiles_hash.second; h = cista::build_hash(hash_file(c.tiles_->profile_), c.tiles_->db_size_); if (c.tiles_->coastline_.has_value()) { h = cista::hash_combine(h, hash_file(*c.tiles_->coastline_)); } } auto const to_clasz_bool_array = [&](bool const default_allowed, std::optional> const& clasz_allowed) { auto a = std::array{}; a.fill(default_allowed); if (clasz_allowed.has_value()) { for (auto const& [clasz, allowed] : *clasz_allowed) { a[static_cast(n::to_clasz(clasz))] = allowed; } } return a; }; auto const route_shapes_clasz_enabled = to_clasz_bool_array( true, c.timetable_.value_or(config::timetable{}) .route_shapes_.value_or(config::timetable::route_shapes{}) .clasz_); auto route_shapes_clasz_hash = std::pair{"route_shapes_clasz"s, cista::BASE_HASH}; for (auto const& b : route_shapes_clasz_enabled) { route_shapes_clasz_hash.second = cista::build_seeded_hash(route_shapes_clasz_hash.second, b); } auto const shape_cache_path = data_path / "routed_shapes_cache.mdb"; auto const shape_cache_lock_path = fs::path{shape_cache_path.generic_string() + "-lock"}; auto const route_shapes_task_enabled = c.timetable_ .transform([](auto&& x) { return x.route_shapes_.has_value(); }) .value_or(false); auto const existing_rs_hashes = read_hashes(data_path, "route_shapes"); auto const route_shapes_reuse_old_osm_data = c.timetable_.value_or(config::timetable{}) .route_shapes_.value_or(config::timetable::route_shapes{}) .cache_reuse_old_osm_data_; auto const reuse_shapes_cache = // cache must exist (handles case where files were deleted manually) fs::exists(shape_cache_path) && // and have the same routed_shapes_ver (existing_rs_hashes.find("routed_shapes_ver") != end(existing_rs_hashes) && existing_rs_hashes.at("routed_shapes_ver") == routed_shapes_version().second) && // if route_shapes_reuse_old_osm_data, we can reuse any data // otherwise only if the osm data is the same (route_shapes_reuse_old_osm_data || (existing_rs_hashes.find(osm_hash.first) != end(existing_rs_hashes) && existing_rs_hashes.at(osm_hash.first) == osm_hash.second && // cache_reuse_old_osm_data flag must be the same or changed from 0->1 // otherwise cache may contain old data from previous runs existing_rs_hashes.find("cache_reuse_old_osm_data") != end(existing_rs_hashes) && (existing_rs_hashes.at("cache_reuse_old_osm_data") == static_cast(route_shapes_reuse_old_osm_data) || existing_rs_hashes.at("cache_reuse_old_osm_data") == 0))); auto const keep_routed_shape_data = !route_shapes_task_enabled || reuse_shapes_cache; if (!keep_routed_shape_data) { fs::remove(shape_cache_path, ec); fs::remove(shape_cache_lock_path, ec); } auto osr = task{"osr", {}, c.use_street_routing(), [&]() { osr::extract(true, fs::path{*c.osm_}, data_path / "osr", elevation_dir); }, {osm_hash, osr_version(), elevation_dir_hash}}; auto adr = task{"adr", {}, c.geocoding_ || c.reverse_geocoding_, [&]() { if (!c.osm_) { return; } adr::extract(*c.osm_, data_path / "adr", data_path / "adr"); }, {osm_hash, adr_version(), {"geocoding", c.geocoding_}, {"reverse_geocoding", c.reverse_geocoding_}}}; auto tt = task{ "tt", {}, c.timetable_.has_value(), [&]() { auto const& t = *c.timetable_; auto const first_day = n::parse_date(t.first_day_) - std::chrono::days{1}; auto const interval = n::interval{ first_day, first_day + std::chrono::days{t.num_days_ + 1U}}; auto assistance = std::unique_ptr{}; if (t.assistance_times_.has_value()) { auto const f = cista::mmap{t.assistance_times_->generic_string().c_str(), cista::mmap::protection::READ}; assistance = std::make_unique( nl::read_assistance(f.view())); } auto shapes = std::unique_ptr{}; if (t.with_shapes_) { shapes = std::make_unique( data_path, cista::mmap::protection::WRITE, keep_routed_shape_data); } auto tags = cista::wrapped{cista::raw::make_unique()}; auto tt = cista::wrapped{cista::raw::make_unique(nl::load( utl::to_vec( t.datasets_, [&, src = n::source_idx_t{}]( std::pair const& x) mutable -> nl::timetable_source { auto const& [tag, dc] = x; tags->add(src++, tag); return { tag, dc.path_, {.link_stop_distance_ = t.link_stop_distance_, .default_tz_ = dc.default_timezone_.value_or( t.default_timezone_.value_or("")), .bikes_allowed_default_ = to_clasz_bool_array( dc.default_bikes_allowed_, dc.clasz_bikes_allowed_), .cars_allowed_default_ = to_clasz_bool_array( dc.default_cars_allowed_, dc.clasz_cars_allowed_), .extend_calendar_ = dc.extend_calendar_, .user_script_ = dc.script_ .and_then([](std::string const& path) { if (path.starts_with("\nfunction")) { return std::optional{path}; } return std::optional{std::string{ cista::mmap{path.c_str(), cista::mmap::protection::READ} .view()}}; }) .value_or("")}}; }), {.adjust_footpaths_ = t.adjust_footpaths_, .merge_dupes_intra_src_ = t.merge_dupes_intra_src_, .merge_dupes_inter_src_ = t.merge_dupes_inter_src_, .max_footpath_length_ = t.max_footpath_length_}, interval, assistance.get(), shapes.get(), false))}; tt->write(data_path / "tt.bin"); tags->write(data_path / "tags.bin"); std::ofstream(data_path / "timetable_metrics.json") << to_str(n::get_metrics(*tt), *tt); }, {tt_hash, n_version()}}; auto tbd = task{"tbd", {&tt}, c.timetable_.has_value() && c.timetable_->tb_, [&]() { auto d = data{data_path}; d.load_tt("tt.bin"); cista::write( data_path / "tbd.bin", n::routing::tb::preprocess(*d.tt_, n::kDefaultProfile)); }, {tt_hash, n_version(), tbd_version()}}; auto adr_extend = task{ "adr_extend", c.osm_.has_value() ? std::vector{&adr, &tt} : std::vector{&tt}, c.timetable_.has_value() && (c.geocoding_ || c.reverse_geocoding_), [&]() { auto d = data{data_path}; d.load_tt("tt.bin"); if (c.osm_) { d.t_ = adr::read(data_path / "adr" / "t.bin"); d.tc_ = std::make_unique(d.t_->strings_.size(), 100U); d.f_ = std::make_unique(); } auto const area_db = d.t_ ? (std::optional{ std::in_place, data_path / "adr", cista::mmap::protection::READ}) : std::nullopt; if (!d.t_) { d.t_ = cista::wrapped{ cista::raw::make_unique()}; } auto const location_extra_place = adr_extend_tt( *d.tt_, area_db.has_value() ? &*area_db : nullptr, *d.t_); auto ec = std::error_code{}; std::filesystem::create_directories(data_path / "adr", ec); cista::write(data_path / "adr" / "t_ext.bin", *d.t_); cista::write(data_path / "adr" / "location_extra_place.bin", location_extra_place); { auto r = adr::reverse{data_path / "adr", cista::mmap::protection::WRITE}; r.build_rtree(*d.t_); r.write(); } cista::free_self_allocated(d.t_.get()); }, {tt_hash, osm_hash, adr_version(), adr_ext_version(), n_version(), {"geocoding", c.geocoding_}, {"reverse_geocoding", c.reverse_geocoding_}}}; auto osr_footpath_settings_hash = meta_entry_t{"osr_footpath_settings", cista::BASE_HASH}; if (c.timetable_) { auto& h = osr_footpath_settings_hash.second; h = cista::hash_combine(h, c.timetable_->use_osm_stop_coordinates_, c.timetable_->extend_missing_footpaths_, c.timetable_->max_matching_distance_, c.timetable_->max_footpath_length_); } auto osr_footpath = task{ "osr_footpath", {&tt, &osr}, c.osr_footpath_ && c.timetable_, [&]() { auto d = data{data_path}; d.load_tt("tt.bin"); d.load_osr(); auto const profiles = std::vector{ {.profile_ = osr::search_profile::kFoot, .profile_idx_ = n::kFootProfile, .max_matching_distance_ = c.timetable_->max_matching_distance_, .extend_missing_ = c.timetable_->extend_missing_footpaths_, .max_duration_ = c.timetable_->max_footpath_length_ * 1min}, {.profile_ = osr::search_profile::kWheelchair, .profile_idx_ = n::kWheelchairProfile, .max_matching_distance_ = 8.0, .max_duration_ = c.timetable_->max_footpath_length_ * 1min}, {.profile_ = osr::search_profile::kCar, .profile_idx_ = n::kCarProfile, .max_matching_distance_ = 250.0, .max_duration_ = 8h, .is_candidate_ = [&](n::location_idx_t const l) { return utl::any_of(d.tt_->location_routes_[l], [&](auto r) { return d.tt_->has_car_transport(r); }); }}}; auto const elevator_footpath_map = compute_footpaths( *d.w_, *d.l_, *d.pl_, *d.tt_, d.elevations_.get(), c.timetable_->use_osm_stop_coordinates_, profiles); cista::write(data_path / "elevator_footpath_map.bin", elevator_footpath_map); d.tt_->write(data_path / "tt_ext.bin"); cista::free_self_allocated(d.tt_.get()); }, {tt_hash, osm_hash, osr_footpath_settings_hash, osr_version(), osr_footpath_version(), n_version()}}; auto matches = task{ "matches", {&tt, &osr, &osr_footpath}, c.timetable_ && c.use_street_routing(), [&]() { auto d = data{data_path}; d.load_tt(c.osr_footpath_ ? "tt_ext.bin" : "tt.bin"); d.load_osr(); auto const progress_tracker = utl::get_active_progress_tracker(); progress_tracker->status("Prepare Platform Matches").out_bounds(0, 30); cista::write(data_path / "matches.bin", get_matches(*d.tt_, *d.pl_, *d.w_)); d.load_matches(); if (c.timetable_.value().preprocess_max_matching_distance_ > 0.0) { progress_tracker->status("Prepare Platform Way Matches") .out_bounds(30, 100); way_matches_storage{ data_path, cista::mmap::protection::WRITE, c.timetable_.value().preprocess_max_matching_distance_} .preprocess_osr_matches(*d.tt_, *d.pl_, *d.w_, *d.l_, *d.matches_); } }, {tt_hash, osm_hash, osr_version(), n_version(), matches_version(), std::pair{"way_matches", cista::build_hash(c.timetable_.value_or(config::timetable{}) .preprocess_max_matching_distance_)}}}; auto route_shapes_task = task{ "route_shapes", {&tt, &osr}, route_shapes_task_enabled, [&]() { auto d = data{data_path}; d.load_tt("tt.bin"); d.load_osr(); auto shape_cache = std::unique_ptr{}; if (reuse_shapes_cache) { std::clog << "loading existing shape cache from " << shape_cache_path << "\n"; } else { std::clog << "creating new shape cache\n"; } shape_cache = std::make_unique( shape_cache_path, c.timetable_->route_shapes_->cache_db_size_); // re-open in write mode // this needs to be done in two steps, because the files need to be // closed first, before they can be re-opened in write mode (at // least on Windows) d.shapes_ = {}; auto shapes = n::shapes_storage{ data_path, cista::mmap::protection::MODIFY, reuse_shapes_cache}; route_shapes(*d.w_, *d.l_, *d.tt_, shapes, *c.timetable_->route_shapes_, route_shapes_clasz_enabled, shape_cache.get()); }, {tt_hash, osm_hash, osr_version(), n_version(), routed_shapes_version(), route_shapes_clasz_hash, {"route_shapes_mode", static_cast( c.timetable_.value_or(config::timetable{}) .route_shapes_.value_or(config::timetable::route_shapes{}) .mode_)}, {"cache_reuse_old_osm_data", c.timetable_.value_or(config::timetable{}) .route_shapes_.value_or(config::timetable::route_shapes{}) .cache_reuse_old_osm_data_}}}; auto tiles = task{ "tiles", {}, c.tiles_.has_value(), [&]() { auto const progress_tracker = utl::get_active_progress_tracker(); auto const dir = data_path / "tiles"; auto const path = (dir / "tiles.mdb").string(); auto ec = std::error_code{}; fs::create_directories(data_path / "tiles", ec); progress_tracker->status("Clear Database"); ::tiles::clear_database(path, c.tiles_->db_size_); ::tiles::clear_pack_file(path.c_str()); auto db_env = ::tiles::make_tile_database(path.c_str(), c.tiles_->db_size_); ::tiles::tile_db_handle db_handle{db_env}; ::tiles::pack_handle pack_handle{path.c_str()}; { ::tiles::feature_inserter_mt inserter{ ::tiles::dbi_handle{db_handle, db_handle.features_dbi_opener()}, pack_handle}; if (c.tiles_->coastline_.has_value()) { progress_tracker->status("Load Coastlines").out_bounds(0, 20); ::tiles::load_coastlines(db_handle, inserter, c.tiles_->coastline_->generic_string()); } progress_tracker->status("Load Features").out_bounds(20, 70); ::tiles::load_osm(db_handle, inserter, c.osm_->generic_string(), c.tiles_->profile_.generic_string(), dir.generic_string(), c.tiles_->flush_threshold_); } progress_tracker->status("Pack Features").out_bounds(70, 90); ::tiles::pack_features(db_handle, pack_handle); progress_tracker->status("Prepare Tiles").out_bounds(90, 100); ::tiles::prepare_tiles(db_handle, pack_handle, 10); }, {tiles_version(), osm_hash, tiles_hash}}; auto all_tasks = std::vector{&tiles, &osr, &adr, &tt, &tbd, &adr_extend, &osr_footpath, &matches, &route_shapes_task}; auto todo = std::set{}; if (task_filter.has_value()) { auto q = std::vector{}; for (auto const& x : *task_filter) { auto const it = utl::find_if(all_tasks, [&](task* t) { return t->name_ == x; }); utl::verify(it != end(all_tasks) && (*it)->should_run_, "task {} not found or disabled", x); q.push_back(*it); } while (!q.empty()) { auto const next = q.back(); q.resize(q.size() - 1); todo.insert(next); for (auto const& x : next->dependencies_) { if (!todo.contains(x)) { q.push_back(x); } } } } else { todo.insert(begin(all_tasks), end(all_tasks)); } auto tasks = std::vector{begin(todo), end(todo)}; utl::erase_if(tasks, [&](task* t) { if (!t->should_run_ || t->ready_for_load(data_path)) { t->done_ = true; return true; } return false; }); fmt::println("running tasks: {}", tasks | std::views::transform([](task* t) { return *t; })); for (auto* t : tasks) { t->pt_ = utl::activate_progress_tracker(t->name_); } while (!tasks.empty()) { auto const task_it = utl::find_if(tasks, [](task const* t) { return utl::all_of(t->dependencies_, [](task const* t) { return t->done_; }); }); utl::verify( task_it != end(tasks), "no task to run, remaining tasks: {}", tasks | std::views::transform([](task const* t) { return *t; })); (*task_it)->run(data_path); tasks.erase(task_it); } } } // namespace motis ================================================ FILE: src/journey_to_response.cc ================================================ #include "motis/journey_to_response.h" #include #include #include #include #include "utl/enumerate.h" #include "utl/overloaded.h" #include "utl/visit.h" #include "geo/polyline_format.h" #include "nigiri/common/split_duration.h" #include "nigiri/routing/journey.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/gtfsrt_resolve_run.h" #include "nigiri/rt/service_alert.h" #include "nigiri/special_stations.h" #include "nigiri/types.h" #include "adr/typeahead.h" #include "motis-api/motis-api.h" #include "motis/constants.h" #include "motis/flex/flex_output.h" #include "motis/gbfs/gbfs_output.h" #include "motis/gbfs/routing_data.h" #include "motis/odm/odm.h" #include "motis/osr/mode_to_profile.h" #include "motis/osr/street_routing.h" #include "motis/place.h" #include "motis/polyline.h" #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" #include "motis/timetable/time_conv.h" #include "motis/transport_mode_ids.h" namespace n = nigiri; namespace motis { api::ModeEnum to_mode(osr::search_profile const m) { switch (m) { case osr::search_profile::kCarParkingWheelchair: [[fallthrough]]; case osr::search_profile::kCarParking: return api::ModeEnum::CAR_PARKING; case osr::search_profile::kCarDropOffWheelchair: [[fallthrough]]; case osr::search_profile::kCarDropOff: return api::ModeEnum::CAR_DROPOFF; case osr::search_profile::kFoot: [[fallthrough]]; case osr::search_profile::kWheelchair: return api::ModeEnum::WALK; case osr::search_profile::kCar: return api::ModeEnum::CAR; case osr::search_profile::kBikeElevationLow: [[fallthrough]]; case osr::search_profile::kBikeElevationHigh: [[fallthrough]]; case osr::search_profile::kBikeFast: [[fallthrough]]; case osr::search_profile::kBike: return api::ModeEnum::BIKE; case osr::search_profile::kBikeSharing: [[fallthrough]]; case osr::search_profile::kCarSharing: return api::ModeEnum::RENTAL; case osr::search_profile::kBus: return api::ModeEnum::DEBUG_BUS_ROUTE; case osr::search_profile::kRailway: return api::ModeEnum::DEBUG_RAILWAY_ROUTE; case osr::search_profile::kFerry: return api::ModeEnum::DEBUG_FERRY_ROUTE; } std::unreachable(); } void cleanup_intermodal(api::Itinerary& i) { if (i.legs_.front().from_.name_ == "END") { i.legs_.front().from_.name_ = "START"; } if (i.legs_.back().to_.name_ == "START") { i.legs_.back().to_.name_ = "END"; } } struct fare_indices { std::int64_t transfer_idx_; std::int64_t effective_fare_leg_idx_; }; std::optional get_fare_indices( std::optional> const& fares, n::routing::journey::leg const& l) { if (!fares.has_value()) { return std::nullopt; } for (auto const [transfer_idx, transfer] : utl::enumerate(*fares)) { for (auto const [eff_fare_leg_idx, eff_fare_leg] : utl::enumerate(transfer.legs_)) { for (auto const* x : eff_fare_leg.joined_leg_) { if (x == &l) { return fare_indices{static_cast(transfer_idx), static_cast(eff_fare_leg_idx)}; } } } } return std::nullopt; } std::optional> get_alerts( n::rt::frun const& fr, std::optional> const& s, bool const fuzzy_stop, std::optional> const& language) { if (fr.rtt_ == nullptr || !fr.is_scheduled()) { // TODO added return std::nullopt; } auto const& tt = *fr.tt_; auto const* rtt = fr.rtt_; auto const to_time_range = [&](n::interval const x) -> api::TimeRange { return {x.from_, x.to_}; }; auto const to_cause = [](n::alert_cause const x) { return api::AlertCauseEnum{static_cast(x)}; }; auto const to_effect = [](n::alert_effect const x) { return api::AlertEffectEnum{static_cast(x)}; }; auto const convert_to_str = [](std::string_view s) { return std::optional{std::string{s}}; }; auto const to_alert = [&](n::alert_idx_t const x) -> api::Alert { auto const& a = rtt->alerts_; auto const get_translation = [&](auto const& translations) -> std::optional { if (translations.empty()) { return std::nullopt; } else if (!language.has_value()) { return a.strings_.try_get(translations.front().text_) .and_then(convert_to_str); } else { for (auto const& req_lang : *language) { auto const it = utl::find_if( translations, [&](n::alert_translation const translation) { auto const translation_lang = a.strings_.try_get(translation.language_); return translation_lang.has_value() && translation_lang->starts_with(req_lang); }); if (it == end(translations)) { continue; } return a.strings_.try_get(it->text_).and_then(convert_to_str); } return a.strings_.try_get(translations.front().text_) .and_then(convert_to_str); } }; return { .communicationPeriod_ = a.communication_period_[x].empty() ? std::nullopt : std::optional{utl::to_vec(a.communication_period_[x], to_time_range)}, .impactPeriod_ = a.impact_period_[x].empty() ? std::nullopt : std::optional{utl::to_vec(a.impact_period_[x], to_time_range)}, .cause_ = to_cause(a.cause_[x]), .causeDetail_ = get_translation(a.cause_detail_[x]), .effect_ = to_effect(a.effect_[x]), .effectDetail_ = get_translation(a.effect_detail_[x]), .url_ = get_translation(a.url_[x]), .headerText_ = get_translation(a.header_text_[x]).value_or(""), .descriptionText_ = get_translation(a.description_text_[x]).value_or(""), .ttsHeaderText_ = get_translation(a.tts_header_text_[x]), .ttsDescriptionText_ = get_translation(a.tts_description_text_[x]), .imageUrl_ = a.image_[x].empty() ? std::nullopt : a.strings_.try_get(a.image_[x].front().url_) .and_then(convert_to_str), .imageMediaType_ = a.image_[x].empty() ? std::nullopt : a.strings_.try_get(a.image_[x].front().media_type_) .and_then(convert_to_str), .imageAlternativeText_ = get_translation(a.image_alternative_text_[x])}; }; auto const x = s.and_then([](std::pair const& rs_ev) { auto const& [rs, ev_type] = rs_ev; return std::optional{rs.get_trip_idx(ev_type)}; }).value_or(fr.trip_idx()); auto const l = s.and_then([](std::pair const& rs) { return std::optional{rs.first.get_location_idx()}; }).value_or(n::location_idx_t::invalid()); auto alerts = std::vector{}; for (auto const& t : tt.trip_ids_[x]) { auto const src = tt.trip_id_src_[t]; for (auto const& a : rtt->alerts_.get_alerts(tt, src, x, fr.rt_, l, fuzzy_stop)) { alerts.emplace_back(to_alert(a)); } } return alerts.empty() ? std::nullopt : std::optional{std::move(alerts)}; } struct parent_name_hash { bool operator()(n::location_idx_t const l) const { return cista::hash(tt_->get_default_name(tt_->locations_.get_root_idx(l))); } n::timetable const* tt_{nullptr}; }; struct parent_name_eq { bool operator()(n::location_idx_t const a, n::location_idx_t const b) const { return tt_->get_default_name(tt_->locations_.get_root_idx(a)) == tt_->get_default_name(tt_->locations_.get_root_idx(b)); } n::timetable const* tt_{nullptr}; }; using unique_stop_map_t = hash_map; void get_is_unique_stop_name(n::rt::frun const& fr, n::interval const& stops, unique_stop_map_t& is_unique) { is_unique.clear(); for (auto const i : stops) { auto const [it, is_new] = is_unique.emplace(fr[i].get_location_idx(), true); if (!is_new) { it->second = false; } } } api::Itinerary journey_to_response( osr::ways const* w, osr::lookup const* l, osr::platforms const* pl, n::timetable const& tt, tag_lookup const& tags, flex::flex_areas const* fl, elevators const* e, n::rt_timetable const* rtt, platform_matches_t const* matches, osr::elevation_storage const* elevations, n::shapes_storage const* shapes, gbfs::gbfs_routing_data& gbfs_rd, adr_ext const* ae, tz_map_t const* tz_map, n::routing::journey const& j, place_t const& start, place_t const& dest, street_routing_cache_t& cache, osr::bitvec* blocked_mem, bool const car_transfers, osr_parameters const& osr_params, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs, bool const join_interlined_legs, bool const detailed_transfers, bool const detailed_legs, bool const with_fares, bool const with_scheduled_skipped_stops, double const timetable_max_matching_distance, double const max_matching_distance, unsigned const api_version, bool const ignore_start_rental_return_constraints, bool const ignore_dest_rental_return_constraints, n::lang_t const& lang) { utl::verify(!j.legs_.empty(), "journey without legs"); auto const fares = with_fares ? std::optional{n::get_fares(tt, rtt, j)} : std::nullopt; auto const to_fare_media_type = [](n::fares::fare_media::fare_media_type const t) { using fare_media_type = n::fares::fare_media::fare_media_type; switch (t) { case fare_media_type::kNone: return api::FareMediaTypeEnum::NONE; case fare_media_type::kPaper: return api::FareMediaTypeEnum::PAPER_TICKET; case fare_media_type::kCard: return api::FareMediaTypeEnum::TRANSIT_CARD; case fare_media_type::kContactless: return api::FareMediaTypeEnum::CONTACTLESS_EMV; case fare_media_type::kApp: return api::FareMediaTypeEnum::MOBILE_APP; } std::unreachable(); }; auto const to_media = [&](n::fares::fare_media const& m) -> api::FareMedia { return {.fareMediaName_ = m.name_ == n::string_idx_t::invalid() ? std::nullopt : std::optional{std::string{tt.strings_.get(m.name_)}}, .fareMediaType_ = to_fare_media_type(m.type_)}; }; auto const to_rider_category = [&](n::fares::rider_category const& r) -> api::RiderCategory { return {.riderCategoryName_ = std::string{tt.strings_.get(r.name_)}, .isDefaultFareCategory_ = r.is_default_fare_category_, .eligibilityUrl_ = tt.strings_.try_get(r.eligibility_url_) .and_then([](std::string_view s) { return std::optional{std::string{s}}; })}; }; auto const to_products = [&](n::fares const& f, n::fare_product_idx_t const x) -> std::vector { if (x == n::fare_product_idx_t::invalid()) { return {}; } return utl::to_vec( f.fare_products_[x], [&](n::fares::fare_product const& p) -> api::FareProduct { return { .name_ = std::string{tt.strings_.get(p.name_)}, .amount_ = p.amount_, .currency_ = std::string{tt.strings_.get(p.currency_code_)}, .riderCategory_ = p.rider_category_ == n::rider_category_idx_t::invalid() ? std::nullopt : std::optional{to_rider_category( f.rider_categories_[p.rider_category_])}, .media_ = p.media_ == n::fare_media_idx_t::invalid() ? std::nullopt : std::optional{to_media(f.fare_media_[p.media_])}}; }); }; auto const to_rule = [](n::fares::fare_transfer_rule const& x) { switch (x.fare_transfer_type_) { case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAB: return api::FareTransferRuleEnum::AB; case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAPlusAB: return api::FareTransferRuleEnum::A_AB; case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAPlusABPlusB: return api::FareTransferRuleEnum::A_AB_B; } std::unreachable(); }; auto itinerary = api::Itinerary{ .duration_ = to_seconds(j.arrival_time() - j.departure_time()), .startTime_ = j.legs_.front().dep_time_, .endTime_ = j.legs_.back().arr_time_, .transfers_ = std::max( static_cast::difference_type>(0), utl::count_if( j.legs_, [](n::routing::journey::leg const& leg) { return holds_alternative( leg.uses_) || odm::is_odm_leg(leg, kOdmTransportModeId) || odm::is_odm_leg(leg, kRideSharingTransportModeId); }) - 1), .fareTransfers_ = fares.and_then([&](std::vector const& transfers) { return std::optional{utl::to_vec( transfers, [&](n::fare_transfer const& t) -> api::FareTransfer { return {.rule_ = t.rule_.and_then([&](auto&& r) { return std::optional{to_rule(r)}; }), .transferProducts_ = t.rule_.and_then([&](auto&& r) { return t.legs_.empty() ? std::nullopt : std::optional{to_products( tt.fares_[t.legs_.front().src_], r.fare_product_)}; }), .effectiveFareLegProducts_ = utl::to_vec(t.legs_, [&](auto&& l) { return utl::to_vec(l.rule_, [&](auto&& r) { return to_products(tt.fares_[l.src_], r.fare_product_); }); })}; })}; })}; auto const append = [&](api::Itinerary&& x) { itinerary.legs_.insert(end(itinerary.legs_), std::move_iterator{begin(x.legs_)}, std::move_iterator{end(x.legs_)}); }; auto const get_first_run_tz = [&]() -> std::optional { if (j.legs_.size() < 2) { return std::nullopt; } auto const osm_tz = get_tz(tt, ae, tz_map, j.legs_[1].from_); if (osm_tz != nullptr) { return std::optional{osm_tz->name()}; } return utl::visit( j.legs_[1].uses_, [&](n::routing::journey::run_enter_exit const& x) { return n::rt::frun{tt, rtt, x.r_}[0].get_tz_name(n::event_type::kDep); }); }; for (auto const [j_leg_idx, j_leg] : utl::enumerate(j.legs_)) { auto const pred = itinerary.legs_.empty() ? nullptr : &itinerary.legs_.back(); auto const fallback_tz = pred == nullptr ? get_first_run_tz() : pred->to_.tz_; auto const from = pred == nullptr ? to_place(&tt, &tags, w, pl, matches, ae, tz_map, lang, tt_location{j_leg.from_}, start, dest, "", fallback_tz) : pred->to_; auto const to = to_place(&tt, &tags, w, pl, matches, ae, tz_map, lang, tt_location{j_leg.to_}, start, dest, "", fallback_tz); auto is_unique = unique_stop_map_t{0U, parent_name_hash{&tt}, parent_name_eq{&tt}}; auto const to_place = [&](n::rt::run_stop const& s, n::event_type const ev_type) { auto p = ::motis::to_place(&tt, &tags, w, pl, matches, ae, tz_map, lang, s, start, dest); p.alerts_ = get_alerts(*s.fr_, std::pair{s, ev_type}, false, lang); if (auto const it = is_unique.find(s.get_location_idx()); it != end(is_unique) && !it->second) { p.name_ = tt.translate(lang, tt.locations_.names_[s.get_location_idx()]); } return p; }; std::visit( utl::overloaded{ [&](n::routing::journey::run_enter_exit const& t) { auto const fr = n::rt::frun{tt, rtt, t.r_}; auto is_first_part = true; auto const write_run_leg = [&](auto, n::interval const subrange) { auto const common_stops = subrange.intersect(t.stop_range_); if (common_stops.size() <= 1) { return; } get_is_unique_stop_name(fr, common_stops, is_unique); auto const enter_stop = fr[common_stops.from_]; auto const exit_stop = fr[common_stops.to_ - 1U]; auto const color = enter_stop.get_route_color(n::event_type::kDep); auto const& agency = enter_stop.get_provider(n::event_type::kDep); auto const fare_indices = get_fare_indices(fares, j_leg); auto const src = [&]() { if (!fr.is_scheduled()) { return n::source_idx_t::invalid(); } auto const trip = enter_stop.get_trip_idx(n::event_type::kDep); auto const id_idx = tt.trip_ids_[trip].front(); return tt.trip_id_src_[id_idx]; }(); auto const [service_day, _] = enter_stop.get_trip_start(n::event_type::kDep); auto& leg = itinerary.legs_.emplace_back(api::Leg{ .mode_ = to_mode(enter_stop.get_clasz(n::event_type::kDep), api_version), .from_ = to_place(enter_stop, n::event_type::kDep), .to_ = to_place(exit_stop, n::event_type::kArr), .duration_ = std::chrono::duration_cast( exit_stop.time(n::event_type::kArr) - enter_stop.time(n::event_type::kDep)) .count(), .startTime_ = enter_stop.time(n::event_type::kDep), .endTime_ = exit_stop.time(n::event_type::kArr), .scheduledStartTime_ = enter_stop.scheduled_time(n::event_type::kDep), .scheduledEndTime_ = exit_stop.scheduled_time(n::event_type::kArr), .realTime_ = fr.is_rt(), .scheduled_ = fr.is_scheduled(), .interlineWithPreviousLeg_ = !is_first_part, .headsign_ = std::string{enter_stop.direction( lang, n::event_type::kDep)}, .tripFrom_ = [&]() { auto const first = exit_stop.get_first_trip_stop( n::event_type::kArr); auto p = to_place(first, n::event_type::kDep); p.departure_ = first.time(n::event_type::kDep); p.scheduledDeparture_ = first.scheduled_time(n::event_type::kDep); return p; }(), .tripTo_ = [&]() { auto const last = enter_stop.get_last_trip_stop( n::event_type::kDep); auto p = to_place(last, n::event_type::kArr); p.arrival_ = last.time(n::event_type::kArr); p.scheduledArrival_ = last.scheduled_time(n::event_type::kArr); return p; }(), .category_ = enter_stop.get_category(n::event_type::kDep) .transform([&](nigiri::category_idx_t const c) { auto const& cat = tt.categories_.at(c); return api::Category{ .id_ = std::string{tt.strings_.get(cat.id_)}, .name_ = std::string{tt.translate(lang, cat.name_)}, .shortName_ = std::string{tt.translate( lang, cat.short_name_)}, }; }), .routeId_ = tags.route_id(enter_stop, n::event_type::kDep), .routeUrl_ = std::string{enter_stop.route_url( n::event_type::kDep, lang)}, .directionId_ = enter_stop.get_direction_id(n::event_type::kDep) == 0 ? "0" : "1", .routeColor_ = to_str(color.color_), .routeTextColor_ = to_str(color.text_color_), .routeType_ = enter_stop.route_type(n::event_type::kDep) .transform([](auto&& x) { return to_idx(x); }), .agencyName_ = std::string{tt.translate(lang, agency.name_)}, .agencyUrl_ = std::string{tt.translate(lang, agency.url_)}, .agencyId_ = std::string{ tt.strings_.try_get(agency.id_).value_or("?")}, .tripId_ = tags.id(tt, enter_stop, n::event_type::kDep), .routeShortName_ = {std::string{ api_version > 3 ? enter_stop.route_short_name( n::event_type::kDep, lang) : enter_stop.display_name( n::event_type::kDep, lang)}}, .routeLongName_ = {std::string{ enter_stop.route_long_name(n::event_type::kDep, lang)}}, .tripShortName_ = {std::string{ enter_stop.trip_short_name(n::event_type::kDep, lang)}}, .displayName_ = {std::string{ enter_stop.display_name(n::event_type::kDep, lang)}}, .cancelled_ = fr.is_cancelled(), .source_ = fmt::to_string(fr.dbg()), .fareTransferIndex_ = fare_indices.transform( [](auto&& x) { return x.transfer_idx_; }), .effectiveFareLegIndex_ = fare_indices.transform( [](auto&& x) { return x.effective_fare_leg_idx_; }), .alerts_ = get_alerts(fr, std::nullopt, false, lang), .loopedCalendarSince_ = (fr.is_scheduled() && src != n::source_idx_t::invalid() && tt.src_end_date_[src] < service_day) ? std::optional{tt.src_end_date_[src]} : std::nullopt, .bikesAllowed_ = enter_stop.bikes_allowed(nigiri::event_type::kDep)}); auto const attributes = tt.attribute_combinations_[enter_stop .get_attribute_combination( n::event_type::kDep)]; if (!leg.alerts_ && !attributes.empty()) { leg.alerts_ = std::vector{}; } for (auto const& a : attributes) { leg.alerts_->push_back(api::Alert{ .code_ = std::string{tt.attributes_[a].code_.view()}, .headerText_ = std::string{ tt.translate(lang, tt.attributes_[a].text_)}}); } leg.from_.vertexType_ = api::VertexTypeEnum::TRANSIT; leg.from_.departure_ = leg.startTime_; leg.from_.scheduledDeparture_ = leg.scheduledStartTime_; leg.to_.vertexType_ = api::VertexTypeEnum::TRANSIT; leg.to_.arrival_ = leg.endTime_; leg.to_.scheduledArrival_ = leg.scheduledEndTime_; if (detailed_legs) { auto polyline = geo::polyline{}; fr.for_each_shape_point(shapes, common_stops, [&](geo::latlng const& pos) { polyline.emplace_back(pos); }); leg.legGeometry_ = api_version == 1 ? to_polyline<7>(polyline) : to_polyline<6>(polyline); } else { leg.legGeometry_ = empty_polyline(); } auto const first = static_cast(common_stops.from_ + 1U); auto const last = static_cast(common_stops.to_ - 1U); leg.intermediateStops_ = std::vector{}; for (auto i = first; i < last; ++i) { auto const stop = fr[i]; if (!with_scheduled_skipped_stops && !stop.get_scheduled_stop().in_allowed() && !stop.get_scheduled_stop().out_allowed() && !stop.in_allowed() && !stop.out_allowed()) { continue; } auto& p = leg.intermediateStops_->emplace_back( to_place(stop, n::event_type::kDep)); p.departure_ = stop.time(n::event_type::kDep); p.scheduledDeparture_ = stop.scheduled_time(n::event_type::kDep); p.arrival_ = stop.time(n::event_type::kArr); p.scheduledArrival_ = stop.scheduled_time(n::event_type::kArr); } is_first_part = false; }; if (join_interlined_legs) { write_run_leg(n::trip_idx_t{}, t.stop_range_); } else { fr.for_each_trip(write_run_leg); } }, [&](n::footpath) { append(w && l && detailed_transfers ? street_routing( *w, *l, e, elevations, lang, from, to, default_output{ *w, car_transfers ? osr::search_profile::kCar : to_profile(api::ModeEnum::WALK, pedestrian_profile, elevation_costs)}, j_leg.dep_time_, j_leg.arr_time_, car_transfers ? 250.0 : timetable_max_matching_distance, osr_params, cache, *blocked_mem, api_version, true, std::chrono::duration_cast( j_leg.arr_time_ - j_leg.dep_time_) + std::chrono::minutes{10}) : dummy_itinerary(from, to, api::ModeEnum::WALK, j_leg.dep_time_, j_leg.arr_time_)); }, [&](n::routing::offset const x) { if ((j_leg_idx == 0 || j_leg_idx == j.legs_.size() - 1) && j_leg.dep_time_ == j_leg.arr_time_) { return; } auto out = std::unique_ptr{}; if (flex::mode_id::is_flex(x.transport_mode_id_)) { out = std::make_unique( *w, *l, pl, matches, ae, tz_map, tags, tt, *fl, flex::mode_id{x.transport_mode_id_}); } else if (x.transport_mode_id_ >= kGbfsTransportModeIdOffset) { auto const is_pre_transit = pred == nullptr; out = std::make_unique( *w, gbfs_rd, gbfs_rd.get_products_ref(x.transport_mode_id_), is_pre_transit ? ignore_start_rental_return_constraints : ignore_dest_rental_return_constraints); } else { out = std::make_unique(*w, x.transport_mode_id_); } append(street_routing( *w, *l, e, elevations, lang, from, to, *out, j_leg.dep_time_, j_leg.arr_time_, max_matching_distance, osr_params, cache, *blocked_mem, api_version, detailed_legs, std::chrono::duration_cast( j_leg.arr_time_ - j_leg.dep_time_) + std::chrono::minutes{5})); }}, j_leg.uses_); } cleanup_intermodal(itinerary); return itinerary; } } // namespace motis ================================================ FILE: src/logging.cc ================================================ #include "motis/logging.h" #include #include #include #include "fmt/ostream.h" #include "utl/logging.h" #include "nigiri/logging.h" namespace motis { int set_log_level(std::string_view log_lvl) { if (log_lvl == "error") { utl::log_verbosity = utl::log_level::error; nigiri::s_verbosity = nigiri::log_lvl::error; } else if (log_lvl == "info") { utl::log_verbosity = utl::log_level::info; nigiri::s_verbosity = nigiri::log_lvl::info; } else if (log_lvl == "debug") { utl::log_verbosity = utl::log_level::debug; nigiri::s_verbosity = nigiri::log_lvl::debug; } else { fmt::println(std::cerr, "Unsupported log level '{}'\n", log_lvl); return 1; } return 0; }; int set_log_level(config const& c) { if (c.logging_ && c.logging_->log_level_) { return set_log_level(*c.logging_->log_level_); } return 0; } int set_log_level(std::string&& log_lvl) { // Support uppercase for command line option std::transform(log_lvl.begin(), log_lvl.end(), log_lvl.begin(), [](unsigned char const c) { return std::tolower(c); }); return set_log_level(log_lvl); } } // namespace motis ================================================ FILE: src/match_platforms.cc ================================================ #include "motis/match_platforms.h" #include #include "utl/helpers/algorithm.h" #include "utl/parallel_for.h" #include "utl/parser/arg_parser.h" #include "osr/geojson.h" #include "osr/location.h" #include "motis/location_routes.h" #include "motis/osr/parameters.h" namespace n = nigiri; namespace motis { bool is_number(char const x) { return x >= '0' && x <= '9'; } template void for_each_number(std::string_view x, Fn&& fn) { for (auto i = 0U; i < x.size(); ++i) { if (!is_number(x[i])) { continue; } auto j = i + 1U; for (; j != x.size(); ++j) { if (!is_number(x[j])) { break; } } fn(utl::parse(x.substr(i, j - i))); i = j; } } bool has_number_match(std::string_view a, std::string_view b) { auto match = false; for_each_number(a, [&](unsigned const x) { for_each_number(b, [&](unsigned const y) { match = (x == y); }); }); return match; } template bool has_number_match(Collection&& a, std::string_view b) { return std::any_of(a.begin(), a.end(), [&](auto&& x) { return has_number_match(x.view(), b); }); } template bool has_exact_match(Collection&& a, std::string_view b) { return std::any_of(a.begin(), a.end(), [&](auto&& x) { return x.view() == b; }); } template bool has_contains_match(Collection&& a, std::string_view b) { return std::any_of(a.begin(), a.end(), [&](auto&& x) { return x.view().contains(b); }); } std::optional get_track(std::string_view s) { if (s.size() == 0 || std::isdigit(s.back()) == 0) { return std::nullopt; } for (auto i = 0U; i != s.size(); ++i) { auto const j = s.size() - i - 1U; if (std::isdigit(s[j]) == 0U) { return s.substr(j + 1U); } } return s; } template double get_routes_bonus(n::timetable const& tt, n::location_idx_t const l, Collection&& names) { auto matches = 0U; for (auto const& r : get_location_routes(tt, l)) { for (auto const& x : names) { if (r == x.view()) { ++matches; } utl::for_each_token(x.view(), ' ', [&](auto&& token) { if (r == token.view()) { ++matches; } }); } } return matches * 20U; } template double get_match_bonus(Collection&& names, std::string_view ref, std::string_view name) { auto bonus = 0U; auto const size = static_cast(name.size()); if (has_exact_match(names, ref)) { bonus += std::max(0.0, 200.0 - size); } if (has_number_match(names, name)) { bonus += std::max(0.0, 140.0 - size); } if (auto const track = get_track(ref); track.has_value() && has_number_match(names, *track)) { bonus += std::max(0.0, 20.0 - size); } if (has_exact_match(names, name)) { bonus += std::max(0.0, 15.0 - size); } if (has_contains_match(names, ref)) { bonus += std::max(0.0, 5.0 - size); } return bonus; } template int compare_platform_code(Collection&& names, std::string_view platform_code) { auto first_match = true; auto bonus = 0; for (auto const& x : names) { if (std::string_view{x} == platform_code) { bonus += first_match ? 50 : 15; first_match = false; } } return bonus; } struct center { template void add(T const& polyline) { for (auto const& x : polyline) { add(geo::latlng(x)); } } void add(geo::latlng const& pos) { sum_.lat_ += pos.lat(); sum_.lng_ += pos.lng(); n_ += 1U; } geo::latlng get_center() const { return {sum_.lat_ / n_, sum_.lng_ / n_}; } geo::latlng sum_; std::size_t n_; }; std::optional get_platform_center(osr::platforms const& pl, osr::ways const& w, osr::platform_idx_t const x) { auto c = center{}; for (auto const p : pl.platform_ref_[x]) { std::visit(utl::overloaded{[&](osr::node_idx_t const node) { c.add(pl.get_node_pos(node).as_latlng()); }, [&](osr::way_idx_t const way) { c.add(w.way_polylines_[way]); }}, osr::to_ref(p)); } if (c.n_ == 0U) { return std::nullopt; } auto const center = c.get_center(); auto const lng_dist = geo::approx_distance_lng_degrees(center); auto closest = geo::latlng{}; auto update_closest = [&, squared_dist = std::numeric_limits::max()]( geo::latlng const& candidate, double const candidate_squared_dist) mutable { if (candidate_squared_dist < squared_dist) { closest = candidate; squared_dist = candidate_squared_dist; } }; for (auto const p : pl.platform_ref_[x]) { std::visit( utl::overloaded{ [&](osr::node_idx_t const node) { auto const candidate = pl.get_node_pos(node).as_latlng(); update_closest(candidate, geo::approx_squared_distance( candidate, center, lng_dist)); }, [&](osr::way_idx_t const way) { for (auto const [a, b] : utl::pairwise(w.way_polylines_[way])) { auto const [best, squared_dist] = geo::approx_closest_on_segment(center, a, b, lng_dist); update_closest(best, squared_dist); } }}, osr::to_ref(p)); } return closest; } vector_map get_matches( nigiri::timetable const& tt, osr::platforms const& pl, osr::ways const& w) { auto m = n::vector_map{}; m.resize(tt.n_locations()); utl::parallel_for_run(tt.n_locations(), [&](auto const i) { auto const l = n::location_idx_t{i}; m[l] = get_match(tt, pl, w, l); }); return m; } osr::platform_idx_t get_match(n::timetable const& tt, osr::platforms const& pl, osr::ways const& w, n::location_idx_t const l) { auto const ref = tt.locations_.coordinates_[l]; auto best = osr::platform_idx_t::invalid(); auto best_score = std::numeric_limits::max(); pl.find(ref, [&](osr::platform_idx_t const x) { auto const center = get_platform_center(pl, w, x); if (!center.has_value()) { return; } auto const dist = geo::distance(*center, ref); auto const match_bonus = get_match_bonus(pl.platform_names_[x], tt.locations_.ids_[l].view(), tt.get_default_translation(tt.locations_.names_[l])); auto const lvl = pl.get_level(w, x); auto const lvl_bonus = lvl != osr::kNoLevel && lvl.to_float() != 0.0F ? 5 : 0; auto const way_bonus = osr::is_way(pl.platform_ref_[x].front()) ? 20 : 0; auto const routes_bonus = get_routes_bonus(tt, l, pl.platform_names_[x]); auto const code_bonus = compare_platform_code( pl.platform_names_[x], tt.get_default_translation(tt.locations_.platform_codes_[l])); auto const score = dist - match_bonus - way_bonus - lvl_bonus - routes_bonus - code_bonus; if (score < best_score) { best = x; best_score = score; } }); if (best != osr::platform_idx_t::invalid()) { get_match_bonus(pl.platform_names_[best], tt.locations_.ids_[l].view(), tt.get_default_translation(tt.locations_.names_[l])); } return best; } way_matches_storage::way_matches_storage(std::filesystem::path path, cista::mmap::protection const mode, double const max_matching_distance) : mode_{mode}, p_{[&]() { std::filesystem::create_directories(path); return std::move(path); }()}, matches_{osr::mm_vec{mm("way_matches.bin")}, osr::mm_vec>{ mm("way_matches_idx.bin")}}, max_matching_distance_{max_matching_distance} {} cista::mmap way_matches_storage::mm(char const* file) { return cista::mmap{(p_ / file).generic_string().c_str(), mode_}; } void way_matches_storage::preprocess_osr_matches( nigiri::timetable const& tt, osr::platforms const& pl, osr::ways const& w, osr::lookup const& lookup, platform_matches_t const& platform_matches) { auto const pt = utl::get_active_progress_tracker(); pt->in_high(tt.n_locations()); utl::parallel_ordered_collect_threadlocal( tt.n_locations(), [&](int, std::size_t const idx) { auto const l = n::location_idx_t{static_cast(idx)}; return lookup.get_raw_match( osr::location{tt.locations_.coordinates_[l], pl.get_level(w, platform_matches[l])}, max_matching_distance_); }, [&](std::size_t, std::vector&& l) { matches_.emplace_back(l); }, pt->update_fn()); } std::vector get_reverse_platform_way_matches( osr::lookup const& lookup, way_matches_storage const* way_matches, osr::search_profile const p, std::span const locations, std::span const osr_locations, osr::direction const dir, double const max_matching_distance) { auto const use_raw_matches = way_matches && !way_matches->matches_.empty() && way_matches->max_matching_distance_ >= max_matching_distance; return utl::to_vec( utl::zip(locations, osr_locations), [&](std::tuple const ll) { auto const& [l, query] = ll; auto raw_matches = std::optional>{}; if (use_raw_matches) { auto const& m = way_matches->matches_[l]; raw_matches = {m.begin(), m.end()}; } return lookup.match(to_profile_parameters(p, {}), query, true, dir, max_matching_distance, nullptr, p, raw_matches); }); }; } // namespace motis ================================================ FILE: src/metrics_registry.cc ================================================ #include "motis/metrics_registry.h" #include "prometheus/histogram.h" namespace motis { metrics_registry::metrics_registry() : metrics_registry( prometheus::Histogram::BucketBoundaries{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 75, 100, 1000}, prometheus::Histogram::BucketBoundaries{ 0.01, 0.1, 0.5, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 45, 60}) {} metrics_registry::metrics_registry( prometheus::Histogram::BucketBoundaries event_boundaries, prometheus::Histogram::BucketBoundaries time_boundaries) : registry_{prometheus::Registry()}, routing_requests_{prometheus::BuildCounter() .Name("motis_routing_requests_total") .Help("Number of routing requests") .Register(registry_) .Add({})}, one_to_many_requests_{prometheus::BuildCounter() .Name("motis_one_to_many_requests_total") .Help("Number of one to many requests") .Register(registry_) .Add({})}, routing_journeys_found_{prometheus::BuildCounter() .Name("motis_routing_journeys_found_total") .Help("Number of journey results") .Register(registry_) .Add({})}, routing_odm_journeys_found_{ prometheus::BuildHistogram() .Name("motis_routing_odm_events_found") .Help("Number of journey results including an ODM part") .Register(registry_)}, routing_odm_journeys_found_blacklist_{routing_odm_journeys_found_.Add( {{"stage", "blacklist"}}, prometheus::Histogram::BucketBoundaries{0, 500, 1000, 2000, 3000, 4000, 5000, 10000, 15000, 20000, 50000})}, routing_odm_journeys_found_whitelist_{routing_odm_journeys_found_.Add( {{"stage", "whitelist"}}, event_boundaries)}, routing_odm_journeys_found_non_dominated_pareto_{ routing_odm_journeys_found_.Add({{"stage", "non_dominated_pareto"}}, event_boundaries)}, routing_odm_journeys_found_non_dominated_cost_{ routing_odm_journeys_found_.Add({{"stage", "non_dominated_cost"}}, event_boundaries)}, routing_odm_journeys_found_non_dominated_prod_{ routing_odm_journeys_found_.Add({{"stage", "non_dominated_prod"}}, event_boundaries)}, routing_odm_journeys_found_non_dominated_{routing_odm_journeys_found_.Add( {{"stage", "non_dominated"}}, event_boundaries)}, routing_journey_duration_seconds_{ prometheus::BuildHistogram() .Name("motis_routing_journey_duration_seconds") .Help("Journey duration statistics") .Register(registry_) .Add({}, prometheus::Histogram::BucketBoundaries{ 300, 600, 1200, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 43200, 86400})}, routing_execution_duration_seconds_{ prometheus::BuildHistogram() .Name("motis_routing_execution_duration_seconds") .Help("Routing execution duration statistics") .Register(registry_)}, routing_execution_duration_seconds_init_{ routing_execution_duration_seconds_.Add({{"stage", "init"}}, time_boundaries)}, routing_execution_duration_seconds_blacklisting_{ routing_execution_duration_seconds_.Add({{"stage", "blacklisting"}}, time_boundaries)}, routing_execution_duration_seconds_preparing_{ routing_execution_duration_seconds_.Add({{"stage", "preparing"}}, time_boundaries)}, routing_execution_duration_seconds_routing_{ routing_execution_duration_seconds_.Add({{"stage", "routing"}}, time_boundaries)}, routing_execution_duration_seconds_whitelisting_{ routing_execution_duration_seconds_.Add({{"stage", "whitelisting"}}, time_boundaries)}, routing_execution_duration_seconds_mixing_{ routing_execution_duration_seconds_.Add({{"stage", "mixing"}}, time_boundaries)}, routing_execution_duration_seconds_total_{ routing_execution_duration_seconds_.Add({{"stage", "total"}}, time_boundaries)}, current_trips_running_scheduled_count_{ prometheus::BuildGauge() .Name("current_trips_running_scheduled_count") .Help("The number of currently running transports") .Register(registry_)}, current_trips_running_scheduled_with_realtime_count_{ prometheus::BuildGauge() .Name("current_trips_running_scheduled_with_realtime_count") .Help("The number of currently running transports that have RT " "data") .Register(registry_)}, total_trips_with_realtime_count_{ prometheus::BuildGauge() .Name("total_trips_with_realtime_count") .Help("The total number of transports that have RT data") .Register(registry_) .Add({})}, timetable_first_day_timestamp_{ prometheus::BuildGauge() .Name("nigiri_timetable_first_day_timestamp_seconds") .Help("Day of the first transport in unixtime") .Register(registry_)}, timetable_last_day_timestamp_{ prometheus::BuildGauge() .Name("nigiri_timetable_last_day_timestamp_seconds") .Help("Day of the last transport in unixtime") .Register(registry_)}, timetable_locations_count_{ prometheus::BuildGauge() .Name("nigiri_timetable_locations_count") .Help("The number of locations in the timetable") .Register(registry_)}, timetable_trips_count_{prometheus::BuildGauge() .Name("nigiri_timetable_trips_count") .Help("The number of trips in the timetable") .Register(registry_)}, timetable_transports_x_days_count_{ prometheus::BuildGauge() .Name("nigiri_timetable_transports_x_days_count") .Help("The number of transports x service days in the timetable") .Register(registry_)} {} metrics_registry::~metrics_registry() = default; } // namespace motis ================================================ FILE: src/odm/blacklist_taxi.cc ================================================ #include "motis/odm/prima.h" #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/asio/io_context.hpp" #include "boost/json.hpp" #include "nigiri/timetable.h" #include "utl/erase_if.h" #include "motis/http_req.h" namespace n = nigiri; namespace json = boost::json; using namespace std::chrono_literals; namespace motis::odm { json::array to_json(std::vector const& offsets, n::timetable const& tt) { auto a = json::array{}; for (auto const& o : offsets) { auto const& pos = tt.locations_.coordinates_[o.target_]; a.emplace_back(json::value{{"lat", pos.lat_}, {"lng", pos.lng_}}); } return a; } std::string prima::make_blacklist_taxi_request( n::timetable const& tt, n::interval const& taxi_intvl) const { return json::serialize(json::value{ {"start", {{"lat", from_.pos_.lat_}, {"lng", from_.pos_.lng_}}}, {"target", {{"lat", to_.pos_.lat_}, {"lng", to_.pos_.lng_}}}, {"startBusStops", to_json(first_mile_taxi_, tt)}, {"targetBusStops", to_json(last_mile_taxi_, tt)}, {"earliest", to_millis(taxi_intvl.from_)}, {"latest", to_millis(taxi_intvl.to_)}, {"startFixed", fixed_ == n::event_type::kDep}, {"capacities", json::value_from(cap_)}}); } n::interval read_intvl(json::value const& jv) { return n::interval{to_unix(jv.as_object().at("startTime").as_int64()), to_unix(jv.as_object().at("endTime").as_int64())}; } bool prima::consume_blacklist_taxi_response(std::string_view json) { auto const read_service_times = [&](json::array const& blacklist_times, auto const& offsets, auto& taxi_times) { if (blacklist_times.size() != offsets.size()) { n::log(n::log_lvl::debug, "motis.prima", "[blacklist taxi] #intervals mismatch"); taxi_times.clear(); return; } taxi_times.resize(offsets.size()); for (auto [blacklist_time, taxi_time] : utl::zip(blacklist_times, taxi_times)) { for (auto const& t : blacklist_time.as_array()) { taxi_time.emplace_back(read_intvl(t)); } } }; auto const update_direct_rides = [&](json::array const& direct_times) { utl::erase_if(direct_taxi_, [&](auto const& ride) { return utl::none_of(direct_times, [&](auto const& t) { auto const i = read_intvl(t); return i.contains(ride.dep_) && i.contains(ride.arr_); }); }); }; try { auto const o = json::parse(json).as_object(); read_service_times(o.at("start").as_array(), first_mile_taxi_, first_mile_taxi_times_); read_service_times(o.at("target").as_array(), last_mile_taxi_, last_mile_taxi_times_); if (direct_duration_ && *direct_duration_ < kODMMaxDuration) { update_direct_rides(o.at("direct").as_array()); } } catch (std::exception const&) { n::log(n::log_lvl::debug, "motis.prima", "[blacklist taxi] could not parse response: {}", json); return false; } return true; } bool prima::blacklist_taxi(n::timetable const& tt, n::interval const& taxi_intvl) { auto blacklist_response = std::optional{}; auto ioc = boost::asio::io_context{}; try { n::log(n::log_lvl::debug, "motis.prima", "[blacklist taxi] request for {}", taxi_intvl); boost::asio::co_spawn( ioc, [&]() -> boost::asio::awaitable { auto const prima_msg = co_await http_POST( taxi_blacklist_, kReqHeaders, make_blacklist_taxi_request(tt, taxi_intvl), 10s); blacklist_response = get_http_body(prima_msg); }, boost::asio::detached); ioc.run(); } catch (std::exception const& e) { n::log(n::log_lvl::debug, "motis.prima", "[blacklist taxi] networking failed: {}", e.what()); blacklist_response = std::nullopt; } if (!blacklist_response) { return false; } return consume_blacklist_taxi_response(*blacklist_response); } } // namespace motis::odm ================================================ FILE: src/odm/bounds.cc ================================================ #include "motis/odm/bounds.h" #include "tg.h" #include "fmt/std.h" #include "utl/verify.h" #include "cista/mmap.h" namespace fs = std::filesystem; namespace motis::odm { bounds::bounds(fs::path const& p) { auto const f = cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}; geom_ = tg_parse_geojsonn(f.view().data(), f.size()); if (tg_geom_error(geom_)) { char const* err = tg_geom_error(geom_); fmt::println("Error parsing ODM Bounds GeoJSON: {}", err); tg_geom_free(geom_); throw utl::fail("unable to parse {}: {}", p, err); } return; } bounds::~bounds() { tg_geom_free(geom_); } bool bounds::contains(geo::latlng const& x) const { auto const point = tg_geom_new_point(tg_point{x.lng(), x.lat()}); auto const result = tg_geom_within(point, geom_); tg_geom_free(point); return result; } } // namespace motis::odm ================================================ FILE: src/odm/journeys.cc ================================================ #include "motis/odm/journeys.h" #include #include "utl/parser/buf_reader.h" #include "utl/parser/csv_range.h" #include "utl/parser/line_range.h" #include "nigiri/common/parse_time.h" #include "nigiri/routing/pareto_set.h" #include "motis/odm/odm.h" #include "motis/transport_mode_ids.h" namespace motis::odm { struct csv_journey { utl::csv_col departure_time_; utl::csv_col arrival_time_; utl::csv_col transfers_; utl::csv_col first_mile_mode_; utl::csv_col first_mile_duration_; utl::csv_col last_mile_mode_; utl::csv_col last_mile_duration_; }; std::optional read_transport_mode( std::string_view m) { if (m == "taxi") { return kOdmTransportModeId; } else if (m == "walk") { return kWalkTransportModeId; } else { return std::nullopt; } } nigiri::routing::journey make_dummy( nigiri::unixtime_t const departure, nigiri::unixtime_t const arrival, std::uint8_t const transfers, nigiri::transport_mode_id_t const first_mile_mode, nigiri::duration_t const first_mile_duration, nigiri::transport_mode_id_t const last_mile_mode, nigiri::duration_t const last_mile_duration) { return nigiri::routing::journey{ .legs_ = {{nigiri::direction::kForward, nigiri::location_idx_t::invalid(), nigiri::location_idx_t::invalid(), departure, departure + first_mile_duration, nigiri::routing::offset{nigiri::location_idx_t::invalid(), first_mile_duration, first_mile_mode}}, {nigiri::direction::kForward, nigiri::location_idx_t::invalid(), nigiri::location_idx_t::invalid(), arrival - last_mile_duration, arrival, nigiri::routing::offset{nigiri::location_idx_t::invalid(), last_mile_duration, last_mile_mode}}}, .start_time_ = departure, .dest_time_ = arrival, .transfers_ = transfers}; } std::vector from_csv(std::string_view const csv) { auto journeys = std::vector{}; utl::line_range{utl::make_buf_reader(csv)} | utl::csv() | utl::for_each([&](csv_journey const& cj) { try { auto const departure = nigiri::parse_time(cj.departure_time_->trim().view(), "%F %R"); auto const arrival = nigiri::parse_time(cj.arrival_time_->trim().view(), "%F %R"); auto const first_mile_duration = nigiri::duration_t{*cj.first_mile_duration_}; auto const first_mile_mode = read_transport_mode(cj.first_mile_mode_->trim().view()); if (!first_mile_mode) { fmt::println("Invalid first-mile transport mode: {}", cj.first_mile_mode_->view()); return; } auto const last_mile_duration = nigiri::duration_t{*cj.last_mile_duration_}; auto const last_mile_mode = read_transport_mode(cj.last_mile_mode_->trim().view()); if (!last_mile_mode) { fmt::println("Invalid last-mile transport mode: {}", cj.last_mile_mode_->view()); return; } journeys.push_back(make_dummy(departure, arrival, *cj.transfers_, *first_mile_mode, first_mile_duration, *last_mile_mode, last_mile_duration)); } catch (std::exception const& e) { fmt::println("could not parse csv_journey: {}", e.what()); } }); return journeys; } nigiri::pareto_set separate_pt( std::vector& journeys) { auto pt_journeys = nigiri::pareto_set{}; for (auto j = begin(journeys); j != end(journeys);) { if (is_pure_pt(*j)) { pt_journeys.add(std::move(*j)); j = journeys.erase(j); } else { ++j; } } return pt_journeys; } std::string to_csv(nigiri::routing::journey const& j) { auto const mode_str = [&](nigiri::transport_mode_id_t const mode) { return mode == kOdmTransportModeId ? "taxi" : "walk"; }; auto const first_mile_mode = !j.legs_.empty() && std::holds_alternative( j.legs_.front().uses_) ? mode_str(std::get(j.legs_.front().uses_) .transport_mode_id_) : "walk"; auto const first_mile_duration = !j.legs_.empty() && std::holds_alternative( j.legs_.front().uses_) ? std::get(j.legs_.front().uses_) .duration() .count() : nigiri::duration_t::rep{0}; auto const last_mile_mode = j.legs_.size() > 1 && std::holds_alternative( j.legs_.back().uses_) ? mode_str(std::get(j.legs_.back().uses_) .transport_mode_id_) : "walk"; auto const last_mile_duration = j.legs_.size() > 1 && std::holds_alternative( j.legs_.back().uses_) ? std::get(j.legs_.back().uses_) .duration() .count() : nigiri::duration_t::rep{0}; return fmt::format("{}, {}, {}, {}, {:0>2}, {}, {:0>2}", j.start_time_, j.dest_time_, j.transfers_, first_mile_mode, first_mile_duration, last_mile_mode, last_mile_duration); } std::string to_csv(std::vector const& jv) { auto ss = std::stringstream{}; ss << "departure, arrival, transfers, first_mile_mode, " "first_mile_duration, last_mile_mode, last_mile_duration\n"; for (auto const& j : jv) { ss << to_csv(j) << "\n"; } return ss.str(); } nigiri::routing::journey make_odm_direct(nigiri::location_idx_t const from, nigiri::location_idx_t const to, nigiri::unixtime_t const departure, nigiri::unixtime_t const arrival) { return nigiri::routing::journey{ .legs_ = {{nigiri::direction::kForward, from, to, departure, arrival, nigiri::routing::offset{to, std::chrono::abs(arrival - departure), kOdmTransportModeId}}}, .start_time_ = departure, .dest_time_ = arrival, .dest_ = to, .transfers_ = 0U}; } } // namespace motis::odm ================================================ FILE: src/odm/meta_router.cc ================================================ #if defined(_MSC_VER) // needs to be the first to include WinSock.h #include "boost/asio.hpp" #endif #include "motis/odm/meta_router.h" #include #include "boost/asio/io_context.hpp" #include "boost/thread/tss.hpp" #include "prometheus/histogram.h" #include "utl/erase_duplicates.h" #include "ctx/ctx.h" #include "nigiri/logging.h" #include "nigiri/routing/journey.h" #include "nigiri/routing/limits.h" #include "nigiri/routing/query.h" #include "nigiri/routing/raptor_search.h" #include "nigiri/routing/start_times.h" #include "nigiri/types.h" #include "osr/routing/route.h" #include "motis-api/motis-api.h" #include "motis/constants.h" #include "motis/ctx_data.h" #include "motis/elevators/elevators.h" #include "motis/endpoints/routing.h" #include "motis/gbfs/routing_data.h" #include "motis/journey_to_response.h" #include "motis/metrics_registry.h" #include "motis/odm/bounds.h" #include "motis/odm/journeys.h" #include "motis/odm/odm.h" #include "motis/odm/prima.h" #include "motis/odm/shorten.h" #include "motis/odm/td_offsets.h" #include "motis/osr/parameters.h" #include "motis/osr/street_routing.h" #include "motis/place.h" #include "motis/tag_lookup.h" #include "motis/timetable/modes_to_clasz_mask.h" #include "motis/timetable/time_conv.h" #include "motis/transport_mode_ids.h" namespace n = nigiri; using namespace std::chrono_literals; using td_offsets_t = n::hash_map>; namespace motis::odm { constexpr auto kODMLookAhead = nigiri::duration_t{24h}; constexpr auto kSearchIntervalSize = nigiri::duration_t{10h}; constexpr auto kContextPadding = nigiri::duration_t{2h}; void print_time(auto const& start, std::string_view name, prometheus::Histogram& metric) { auto const millis = std::chrono::duration_cast( std::chrono::steady_clock::now() - start); n::log(n::log_lvl::debug, "motis.prima", "{} {}", name, millis); metric.Observe(static_cast(millis.count()) / 1000.0); } meta_router::meta_router(ep::routing const& r, api::plan_params const& query, std::vector const& pre_transit_modes, std::vector const& post_transit_modes, std::vector const& direct_modes, std::variant const& from, std::variant const& to, api::Place const& from_p, api::Place const& to_p, nigiri::routing::query const& start_time, std::vector& direct, nigiri::duration_t const fastest_direct, bool const odm_pre_transit, bool const odm_post_transit, bool const odm_direct, bool const ride_sharing_pre_transit, bool const ride_sharing_post_transit, bool const ride_sharing_direct, unsigned const api_version) : r_{r}, query_{query}, pre_transit_modes_{pre_transit_modes}, post_transit_modes_{post_transit_modes}, direct_modes_{direct_modes}, from_{from}, to_{to}, from_place_{from_p}, to_place_{to_p}, start_time_{start_time}, direct_{direct}, fastest_direct_{fastest_direct}, odm_pre_transit_{odm_pre_transit}, odm_post_transit_{odm_post_transit}, odm_direct_{odm_direct}, ride_sharing_pre_transit_{ride_sharing_pre_transit}, ride_sharing_post_transit_{ride_sharing_post_transit}, ride_sharing_direct_{ride_sharing_direct}, api_version_{api_version}, tt_{r_.tt_}, rt_{r.rt_}, rtt_{rt_->rtt_.get()}, e_{rt_->e_.get()}, gbfs_rd_{r.w_, r.l_, r.gbfs_}, start_{query_.arriveBy_ ? to_ : from_}, dest_{query_.arriveBy_ ? from_ : to_}, start_modes_{query_.arriveBy_ ? post_transit_modes_ : pre_transit_modes_}, dest_modes_{query_.arriveBy_ ? pre_transit_modes_ : post_transit_modes_}, start_form_factors_{query_.arriveBy_ ? query_.postTransitRentalFormFactors_ : query_.preTransitRentalFormFactors_}, dest_form_factors_{query_.arriveBy_ ? query_.preTransitRentalFormFactors_ : query_.postTransitRentalFormFactors_}, start_propulsion_types_{query_.arriveBy_ ? query_.postTransitRentalPropulsionTypes_ : query_.preTransitRentalPropulsionTypes_}, dest_propulsion_types_{query_.arriveBy_ ? query_.preTransitRentalPropulsionTypes_ : query_.postTransitRentalPropulsionTypes_}, start_rental_providers_{query_.arriveBy_ ? query_.postTransitRentalProviders_ : query_.preTransitRentalProviders_}, dest_rental_providers_{query_.arriveBy_ ? query_.preTransitRentalProviders_ : query_.postTransitRentalProviders_}, start_rental_provider_groups_{ query_.arriveBy_ ? query_.postTransitRentalProviderGroups_ : query_.preTransitRentalProviderGroups_}, dest_rental_provider_groups_{ query_.arriveBy_ ? query_.preTransitRentalProviderGroups_ : query_.postTransitRentalProviderGroups_}, start_ignore_rental_return_constraints_{ query.arriveBy_ ? query_.ignorePreTransitRentalReturnConstraints_ : query_.ignorePostTransitRentalReturnConstraints_}, dest_ignore_rental_return_constraints_{ query.arriveBy_ ? query_.ignorePostTransitRentalReturnConstraints_ : query_.ignorePreTransitRentalReturnConstraints_} {} meta_router::~meta_router() = default; n::routing::query meta_router::get_base_query( n::interval const& intvl) const { return { .start_time_ = intvl, .start_match_mode_ = motis::ep::get_match_mode(r_, start_), .dest_match_mode_ = motis::ep::get_match_mode(r_, dest_), .use_start_footpaths_ = !motis::ep::is_intermodal(r_, start_), .max_transfers_ = static_cast( query_.maxTransfers_.has_value() ? *query_.maxTransfers_ : n::routing::kMaxTransfers), .max_travel_time_ = query_.maxTravelTime_ .and_then([](std::int64_t const dur) { return std::optional{n::duration_t{dur}}; }) .value_or(ep::kInfinityDuration), .min_connection_count_ = 0U, .extend_interval_earlier_ = false, .extend_interval_later_ = false, .max_interval_ = std::nullopt, .prf_idx_ = static_cast( query_.useRoutedTransfers_ ? (query_.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR ? 2U : 1U) : 0U), .allowed_claszes_ = to_clasz_mask(query_.transitModes_), .require_bike_transport_ = query_.requireBikeTransport_, .require_car_transport_ = query_.requireCarTransport_, .transfer_time_settings_ = n::routing::transfer_time_settings{ .default_ = (query_.minTransferTime_ == 0 && query_.additionalTransferTime_ == 0 && query_.transferTimeFactor_ == 1.0), .min_transfer_time_ = n::duration_t{query_.minTransferTime_}, .additional_time_ = n::duration_t{query_.additionalTransferTime_}, .factor_ = static_cast(query_.transferTimeFactor_)}, .via_stops_ = motis::ep::get_via_stops(*tt_, *r_.tags_, query_.via_, query_.viaMinimumStay_, query_.arriveBy_), .fastest_direct_ = fastest_direct_ == ep::kInfinityDuration ? std::nullopt : std::optional{fastest_direct_}}; } std::vector meta_router::search_interval( std::vector const& sub_queries) const { auto const tasks = utl::to_vec(sub_queries, [&](n::routing::query const& q) { auto fn = [&, q = std::move(q)]() mutable { auto const timeout = std::chrono::seconds{query_.timeout_.value_or( r_.config_.get_limits().routing_max_timeout_seconds_)}; auto search_state = n::routing::search_state{}; auto raptor_state = n::routing::raptor_state{}; return routing_result{raptor_search( *tt_, rtt_, search_state, raptor_state, std::move(q), query_.arriveBy_ ? n::direction::kBackward : n::direction::kForward, timeout)}; }; return ctx_call(ctx_data{}, std::move(fn)); }); return utl::to_vec( tasks, [](ctx::future_ptr const& t) { return t->val(); }); } std::vector collect_odm_journeys( std::vector const& results, nigiri::transport_mode_id_t const mode) { auto taxi_journeys = std::vector{}; for (auto const& r : results | std::views::drop(1)) { for (auto const& j : r.journeys_) { if (uses_odm(j, mode)) { taxi_journeys.push_back(j); taxi_journeys.back().transfers_ += (j.legs_.empty() || !is_odm_leg(j.legs_.front(), mode) ? 0U : 1U) + (j.legs_.size() < 2U || !is_odm_leg(j.legs_.back(), mode) ? 0U : 1U); } } } n::log(n::log_lvl::debug, "motis.prima", "[routing] collected {} mixed ODM-PT journeys for mode {}", taxi_journeys.size(), mode); return taxi_journeys; } void pareto_dominance(std::vector& odm_journeys) { auto const pareto_dom = [](n::routing::journey const& a, n::routing::journey const& b) -> bool { auto const odm_time_a = odm_time(a); auto const odm_time_b = odm_time(b); return a.dominates(b) && odm_time_a < odm_time_b; }; for (auto b = begin(odm_journeys); b != end(odm_journeys);) { auto is_dominated = false; for (auto a = begin(odm_journeys); a != end(odm_journeys); ++a) { if (a != b && pareto_dom(*a, *b)) { is_dominated = true; break; } } if (is_dominated) { b = odm_journeys.erase(b); } else { ++b; } } } api::plan_response meta_router::run() { auto const init_start = std::chrono::steady_clock::now(); utl::verify(r_.tt_ != nullptr && r_.tags_ != nullptr, "mode=TRANSIT requires timetable to be loaded"); auto prepare_stats = motis::ep::stats_map_t{}; auto const start_intvl = std::visit( utl::overloaded{[](n::interval const i) { return i; }, [](n::unixtime_t const t) { return n::interval{t, t}; }}, start_time_.start_time_); auto search_intvl = n::interval{start_time_.extend_interval_earlier_ ? start_intvl.to_ - kSearchIntervalSize : start_intvl.from_, start_time_.extend_interval_later_ ? start_intvl.from_ + kSearchIntervalSize : start_intvl.to_}; search_intvl.from_ = r_.tt_->external_interval().clamp(search_intvl.from_); search_intvl.to_ = r_.tt_->external_interval().clamp(search_intvl.to_); auto const context_intvl = n::interval{ search_intvl.from_ - kContextPadding, search_intvl.to_ + kContextPadding}; auto const taxi_intvl = query_.arriveBy_ ? n::interval{context_intvl.from_ - kODMLookAhead, context_intvl.to_} : n::interval{context_intvl.from_, context_intvl.to_ + kODMLookAhead}; auto const to_osr_loc = [&](auto const& place) { return std::visit( utl::overloaded{ [](osr::location const& l) { return l; }, [&](tt_location const& l) { return osr::location{ .pos_ = tt_->locations_.coordinates_[l.l_], .lvl_ = osr::level_t{std::uint8_t{osr::level_t::kNoLevel}}}; }}, place); }; auto p = prima{r_.config_.prima_->url_, to_osr_loc(from_), to_osr_loc(to_), query_}; p.init(search_intvl, taxi_intvl, odm_pre_transit_, odm_post_transit_, odm_direct_, ride_sharing_pre_transit_, ride_sharing_post_transit_, ride_sharing_direct_, *tt_, rtt_, r_, e_, gbfs_rd_, from_place_, to_place_, query_, start_time_, api_version_); std::erase(start_modes_, api::ModeEnum::ODM); std::erase(start_modes_, api::ModeEnum::RIDE_SHARING); std::erase(dest_modes_, api::ModeEnum::ODM); std::erase(dest_modes_, api::ModeEnum::RIDE_SHARING); print_time( init_start, fmt::format("[init] (#first_mile_offsets: {}, #last_mile_offsets: {}, " "#direct_rides: {})", p.first_mile_taxi_.size(), p.last_mile_taxi_.size(), p.direct_taxi_.size()), r_.metrics_->routing_execution_duration_seconds_init_); auto const blacklist_start = std::chrono::steady_clock::now(); auto const blacklisted_taxis = p.blacklist_taxi(*tt_, taxi_intvl); print_time(blacklist_start, fmt::format("[blacklist taxi] (#first_mile_offsets: {}, " "#last_mile_offsets: {}, #direct_rides: {})", p.first_mile_taxi_.size(), p.last_mile_taxi_.size(), p.direct_taxi_.size()), r_.metrics_->routing_execution_duration_seconds_blacklisting_); auto const whitelist_ride_sharing_start = std::chrono::steady_clock::now(); auto const whitelisted_ride_sharing = p.whitelist_ride_sharing(*tt_); n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride-sharing] ride-sharing events after whitelisting: {}", p.n_ride_sharing_events()); print_time( whitelist_ride_sharing_start, fmt::format("[whitelist ride-sharing] (#first_mile_ride_sharing: {}, " "#last_mile_ride_sharing: {}, #direct_ride_sharing: {})", p.first_mile_ride_sharing_.size(), p.last_mile_ride_sharing_.size(), p.direct_ride_sharing_.size()), r_.metrics_->routing_execution_duration_seconds_blacklisting_); auto const prep_queries_start = std::chrono::steady_clock::now(); auto const [first_mile_taxi_short, first_mile_taxi_long] = get_td_offsets_split(p.first_mile_taxi_, p.first_mile_taxi_times_, kOdmTransportModeId); auto const [last_mile_taxi_short, last_mile_taxi_long] = get_td_offsets_split( p.last_mile_taxi_, p.last_mile_taxi_times_, kOdmTransportModeId); auto const params = get_osr_parameters(query_); auto const pre_transit_time = std::min( std::chrono::seconds{query_.maxPreTransitTime_}, std::chrono::seconds{ r_.config_.get_limits().street_routing_max_prepost_transit_seconds_}); auto const post_transit_time = std::min( std::chrono::seconds{query_.maxPostTransitTime_}, std::chrono::seconds{ r_.config_.get_limits().street_routing_max_prepost_transit_seconds_}); auto const qf = query_factory{ .base_query_ = get_base_query(context_intvl), .start_walk_ = r_.get_offsets( rtt_, start_, query_.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward, start_modes_, start_form_factors_, start_propulsion_types_, start_rental_providers_, start_rental_provider_groups_, start_ignore_rental_return_constraints_, params, query_.pedestrianProfile_, query_.elevationCosts_, query_.arriveBy_ ? post_transit_time : pre_transit_time, query_.maxMatchingDistance_, gbfs_rd_, prepare_stats), .dest_walk_ = r_.get_offsets( rtt_, dest_, query_.arriveBy_ ? osr::direction::kForward : osr::direction::kBackward, dest_modes_, dest_form_factors_, dest_propulsion_types_, dest_rental_providers_, dest_rental_provider_groups_, dest_ignore_rental_return_constraints_, params, query_.pedestrianProfile_, query_.elevationCosts_, query_.arriveBy_ ? pre_transit_time : post_transit_time, query_.maxMatchingDistance_, gbfs_rd_, prepare_stats), .td_start_walk_ = r_.get_td_offsets( rtt_, e_, start_, query_.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward, start_modes_, params, query_.pedestrianProfile_, query_.elevationCosts_, query_.maxMatchingDistance_, query_.arriveBy_ ? post_transit_time : pre_transit_time, context_intvl, prepare_stats), .td_dest_walk_ = r_.get_td_offsets( rtt_, e_, dest_, query_.arriveBy_ ? osr::direction::kForward : osr::direction::kBackward, dest_modes_, params, query_.pedestrianProfile_, query_.elevationCosts_, query_.maxMatchingDistance_, query_.arriveBy_ ? pre_transit_time : post_transit_time, context_intvl, prepare_stats), .start_taxi_short_ = query_.arriveBy_ ? last_mile_taxi_short : first_mile_taxi_short, .start_taxi_long_ = query_.arriveBy_ ? last_mile_taxi_long : first_mile_taxi_long, .dest_taxi_short_ = query_.arriveBy_ ? first_mile_taxi_short : last_mile_taxi_short, .dest_taxi_long_ = query_.arriveBy_ ? first_mile_taxi_long : last_mile_taxi_long, .start_ride_sharing_ = query_.arriveBy_ ? get_td_offsets(p.last_mile_ride_sharing_, kRideSharingTransportModeId) : get_td_offsets(p.first_mile_ride_sharing_, kRideSharingTransportModeId), .dest_ride_sharing_ = query_.arriveBy_ ? get_td_offsets(p.first_mile_ride_sharing_, kRideSharingTransportModeId) : get_td_offsets(p.last_mile_ride_sharing_, kRideSharingTransportModeId)}; print_time(prep_queries_start, "[prepare queries]", r_.metrics_->routing_execution_duration_seconds_preparing_); auto const routing_start = std::chrono::steady_clock::now(); auto sub_queries = qf.make_queries(blacklisted_taxis, whitelisted_ride_sharing); n::log(n::log_lvl::debug, "motis.prima", "[prepare queries] {} queries prepared", sub_queries.size()); auto const results = search_interval(sub_queries); utl::verify(!results.empty(), "prima: public transport result expected"); auto const& pt_result = results.front(); auto taxi_journeys = collect_odm_journeys(results, kOdmTransportModeId); shorten(taxi_journeys, p.first_mile_taxi_, p.first_mile_taxi_times_, p.last_mile_taxi_, p.last_mile_taxi_times_, *tt_, rtt_, query_); auto ride_share_journeys = collect_odm_journeys(results, kRideSharingTransportModeId); fix_first_mile_duration(ride_share_journeys, p.first_mile_ride_sharing_, p.first_mile_ride_sharing_, kRideSharingTransportModeId); fix_last_mile_duration(ride_share_journeys, p.last_mile_ride_sharing_, p.last_mile_ride_sharing_, kRideSharingTransportModeId); utl::erase_duplicates( taxi_journeys, std::less{}, [](auto const& a, auto const& b) { return a == b && odm_time(a.legs_.front()) == odm_time(b.legs_.front()) && odm_time(a.legs_.back()) == odm_time(b.legs_.back()); }); n::log(n::log_lvl::debug, "motis.prima", "[routing] interval searched: {}", pt_result.interval_); print_time(routing_start, "[routing]", r_.metrics_->routing_execution_duration_seconds_routing_); auto const whitelist_start = std::chrono::steady_clock::now(); auto const was_whitelist_response_valid = p.whitelist_taxi(taxi_journeys, *tt_); if (was_whitelist_response_valid) { add_direct_odm(p.direct_taxi_, taxi_journeys, from_, to_, query_.arriveBy_, kOdmTransportModeId); } if (whitelisted_ride_sharing) { add_direct_odm(p.direct_ride_sharing_, ride_share_journeys, from_, to_, query_.arriveBy_, kRideSharingTransportModeId); } print_time(whitelist_start, fmt::format("[whitelisting] (#first_mile_taxi: {}, " "#last_mile_taxi: {}, #direct_taxi: {})", p.first_mile_taxi_.size(), p.last_mile_taxi_.size(), p.direct_taxi_.size()), r_.metrics_->routing_execution_duration_seconds_whitelisting_); r_.metrics_->routing_odm_journeys_found_whitelist_.Observe( static_cast(taxi_journeys.size())); pareto_dominance(taxi_journeys); taxi_journeys.insert(end(taxi_journeys), begin(pt_result.journeys_), end(pt_result.journeys_)); taxi_journeys.insert(end(taxi_journeys), begin(ride_share_journeys), end(ride_share_journeys)); utl::sort(taxi_journeys, [](auto const& a, auto const& b) { return std::tuple{a.departure_time(), a.arrival_time(), a.transfers_} < std::tuple{b.departure_time(), b.arrival_time(), b.transfers_}; }); r_.metrics_->routing_journeys_found_.Increment( static_cast(taxi_journeys.size())); r_.metrics_->routing_execution_duration_seconds_total_.Observe( static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now() - init_start) .count()) / 1000.0); if (!taxi_journeys.empty()) { r_.metrics_->routing_journey_duration_seconds_.Observe(static_cast( to_seconds(taxi_journeys.begin()->arrival_time() - taxi_journeys.begin()->departure_time()))); } return { .from_ = from_place_, .to_ = to_place_, .direct_ = std::move(direct_), .itineraries_ = utl::to_vec( taxi_journeys, [&, cache = street_routing_cache_t{}](auto&& j) mutable { if (ep::blocked.get() == nullptr && r_.w_ != nullptr) { ep::blocked.reset( new osr::bitvec{r_.w_->n_nodes()}); } auto const detailed_transfers = query_.detailedTransfers_.value_or(query_.detailedLegs_); auto response = journey_to_response( r_.w_, r_.l_, r_.pl_, *tt_, *r_.tags_, r_.fa_, e_, rtt_, r_.matches_, r_.elevations_, r_.shapes_, gbfs_rd_, r_.ae_, r_.tz_, j, start_, dest_, cache, ep::blocked.get(), query_.requireCarTransport_ && query_.useRoutedTransfers_, params, query_.pedestrianProfile_, query_.elevationCosts_, query_.joinInterlinedLegs_, detailed_transfers, query_.detailedLegs_, query_.withFares_, query_.withScheduledSkippedStops_, r_.config_.timetable_.value().max_matching_distance_, query_.maxMatchingDistance_, api_version_, query_.ignorePreTransitRentalReturnConstraints_, query_.ignorePostTransitRentalReturnConstraints_, query_.language_); if (response.legs_.front().mode_ == api::ModeEnum::RIDE_SHARING && response.legs_.size() == 1) { for (auto const [i, a] : utl::enumerate(p.direct_ride_sharing_)) { if (a.dep_ == response.legs_.front().startTime_ && a.arr_ == response.legs_.front().endTime_) { response.legs_.front().tripId_ = std::optional{ p.direct_ride_sharing_tour_ids_.at(i).view()}; break; } } return response; } if (response.legs_.front().mode_ == api::ModeEnum::RIDE_SHARING) { for (auto const [i, a] : utl::enumerate(p.first_mile_ride_sharing_)) { if (a.time_at_start_ == response.legs_.front() .startTime_ && // not looking at time_at_stop_ // because we would again need to // take into account the 5 min // shift... r_.tags_->id(*tt_, a.stop_) == response.legs_.front().to_.stopId_) { response.legs_.front().tripId_ = std::optional{ p.first_mile_ride_sharing_tour_ids_.at(i).view()}; break; } } } if (response.legs_.back().mode_ == api::ModeEnum::RIDE_SHARING) { for (auto const [i, a] : utl::enumerate(p.last_mile_ride_sharing_)) { if (a.time_at_start_ == response.legs_.back() .endTime_ && // not looking at time_at_stop_ // because we would again need to take // into account the 5 min shift... r_.tags_->id(*tt_, a.stop_) == response.legs_.back().from_.stopId_) { response.legs_.back().tripId_ = std::optional{ p.last_mile_ride_sharing_tour_ids_.at(i).view()}; break; } } } auto const match_times = [&](motis::api::Leg const& leg, boost::json::array const& entries) -> std::optional { auto const it = std::find_if( std::begin(entries), std::end(entries), [&](boost::json::value const& json_entry) { if (json_entry.is_null()) { return false; } auto const& object_entry = json_entry.as_object(); return to_unix(object_entry.at("pickupTime").as_int64()) == leg.startTime_ && to_unix(object_entry.at("dropoffTime").as_int64()) == leg.endTime_; }); if (it != std::end(entries)) { return boost::json::serialize(it->as_object()); } return std::nullopt; }; auto const match_location = [&](motis::api::Leg const& leg, boost::json::array const& outer, std::vector const& locations, bool const check_to) -> std::optional { auto const& stop_id = check_to ? leg.to_.stopId_ : leg.from_.stopId_; for (auto const [loc, outer_value] : utl::zip(locations, outer)) { if (stop_id != r_.tags_->id(*tt_, loc)) { continue; } auto const& inner = outer_value.as_array(); if (auto result = match_times(leg, inner)) { return result; } } return std::nullopt; }; if (!was_whitelist_response_valid) { return response; } if (response.legs_.size() == 1 && response.legs_.front().mode_ == api::ModeEnum::ODM) { if (auto const id = match_times( response.legs_.front(), p.whitelist_response_.at("direct").as_array()); id.has_value()) { response.legs_.front().tripId_ = std::optional{*id}; } return response; } if (!response.legs_.empty() && response.legs_.front().mode_ == api::ModeEnum::ODM) { if (auto const id = match_location( response.legs_.front(), p.whitelist_response_.at("start").as_array(), p.whitelist_first_mile_locations_, true); id.has_value()) { response.legs_.front().tripId_ = std::optional{*id}; } } if (!response.legs_.empty() && response.legs_.back().mode_ == api::ModeEnum::ODM) { if (auto const id = match_location( response.legs_.back(), p.whitelist_response_.at("target").as_array(), p.whitelist_last_mile_locations_, false); id.has_value()) { response.legs_.back().tripId_ = std::optional{*id}; } } return response; }), .previousPageCursor_ = fmt::format("EARLIER|{}", to_seconds(search_intvl.from_)), .nextPageCursor_ = fmt::format("LATER|{}", to_seconds(search_intvl.to_))}; } } // namespace motis::odm ================================================ FILE: src/odm/odm.cc ================================================ #include "motis/odm/odm.h" #include #include "nigiri/routing/journey.h" #include "motis/transport_mode_ids.h" namespace motis::odm { namespace n = nigiri; namespace nr = nigiri::routing; bool by_stop(nr::start const& a, nr::start const& b) { return std::tie(a.stop_, a.time_at_start_, a.time_at_stop_) < std::tie(b.stop_, b.time_at_start_, b.time_at_stop_); } bool is_odm_leg(nr::journey::leg const& l, nigiri::transport_mode_id_t const mode) { return std::holds_alternative(l.uses_) && std::get(l.uses_).transport_mode_id_ == mode; } bool uses_odm(nr::journey const& j, nigiri::transport_mode_id_t const mode) { return utl::any_of(j.legs_, [&](auto const& l) { return is_odm_leg(l, mode); }); } bool is_pure_pt(nr::journey const& j) { return !uses_odm(j, kOdmTransportModeId) && !uses_odm(j, kRideSharingTransportModeId); }; n::duration_t odm_time(nr::journey::leg const& l) { return is_odm_leg(l, kOdmTransportModeId) || is_odm_leg(l, kRideSharingTransportModeId) ? std::get(l.uses_).duration() : n::duration_t{0}; } n::duration_t odm_time(nr::journey const& j) { return std::transform_reduce(begin(j.legs_), end(j.legs_), n::duration_t{0}, std::plus{}, [](auto const& l) { return odm_time(l); }); } n::duration_t pt_time(nr::journey const& j) { return j.travel_time() - odm_time(j); } bool is_direct_odm(nr::journey const& j) { return j.travel_time() == odm_time(j); } n::duration_t duration(nr::start const& ride) { return std::chrono::abs(ride.time_at_stop_ - ride.time_at_start_); } std::string odm_label(nr::journey const& j) { return fmt::format( "[dep: {}, arr: {}, transfers: {}, start_odm: {}, dest_odm: {}]", j.start_time_, j.dest_time_, j.transfers_, odm_time(j.legs_.front()), odm_time(j.legs_.back())); } } // namespace motis::odm ================================================ FILE: src/odm/prima.cc ================================================ #include "motis/odm/prima.h" #include #include "boost/asio/io_context.hpp" #include "boost/json.hpp" #include "utl/erase_if.h" #include "utl/pipes.h" #include "utl/zip.h" #include "nigiri/common/parse_time.h" #include "nigiri/logging.h" #include "nigiri/timetable.h" #include "motis/elevators/elevators.h" #include "motis/endpoints/routing.h" #include "motis/http_req.h" #include "motis/odm/bounds.h" #include "motis/odm/odm.h" #include "motis/transport_mode_ids.h" namespace n = nigiri; namespace nr = nigiri::routing; namespace json = boost::json; namespace motis::odm { prima::prima(std::string const& prima_url, osr::location const& from, osr::location const& to, api::plan_params const& query) : query_{query}, taxi_blacklist_{prima_url + kBlacklistPath}, taxi_whitelist_{prima_url + kWhitelistPath}, ride_sharing_whitelist_{prima_url + kRidesharingPath}, from_{from}, to_{to}, fixed_{query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep}, cap_{ .wheelchairs_ = static_cast( query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR ? 1U : 0U), .bikes_ = static_cast(query.requireBikeTransport_ ? 1 : 0), .passengers_ = query.passengers_.value_or(1U), .luggage_ = query.luggage_.value_or(0U)} {} n::duration_t init_direct(std::vector& rides, ep::routing const& r, elevators const* e, gbfs::gbfs_routing_data& gbfs, api::Place const& from_p, api::Place const& to_p, n::interval const intvl, api::plan_params const& query, unsigned api_version) { auto [_, direct_duration] = r.route_direct( e, gbfs, {}, from_p, to_p, {api::ModeEnum::CAR}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, false, intvl.from_, false, get_osr_parameters(query), query.pedestrianProfile_, query.elevationCosts_, kODMMaxDuration, query.maxMatchingDistance_, kODMDirectFactor, query.detailedLegs_, api_version); auto const step = std::chrono::duration_cast(kODMDirectPeriod); if (direct_duration < kODMMaxDuration) { if (query.arriveBy_) { auto const base_time = intvl.to_ - direct_duration; auto const midnight = std::chrono::floor(base_time); auto const mins_since_midnight = std::chrono::duration_cast(base_time - midnight); auto const floored_5_min = (mins_since_midnight.count() / 5) * 5; auto const start_time = midnight + std::chrono::minutes(floored_5_min); for (auto arr = start_time; intvl.contains(arr); arr -= step) { rides.push_back({.dep_ = arr - direct_duration, .arr_ = arr}); } } else { auto const base_start = intvl.from_; auto const midnight_start = std::chrono::floor(base_start); auto const mins_since_midnight_start = std::chrono::duration_cast(base_start - midnight_start); auto const ceiled_5_min_start = ((mins_since_midnight_start.count() + 4) / 5) * 5; auto const start_time_for_depart = midnight_start + std::chrono::minutes(ceiled_5_min_start); for (auto dep = start_time_for_depart; intvl.contains(dep); dep += step) { rides.push_back({.dep_ = dep, .arr_ = dep + direct_duration}); } } } return direct_duration; } void init_pt(std::vector& offsets, std::vector& rides, ep::routing const& r, osr::location const& l, osr::direction dir, api::plan_params const& query, gbfs::gbfs_routing_data& gbfs_rd, n::timetable const& tt, n::rt_timetable const* rtt, n::interval const& intvl, n::routing::query const& start_time, n::routing::location_match_mode location_match_mode, std::chrono::seconds const max) { auto stats = std::map{}; offsets = r.get_offsets(rtt, l, dir, {api::ModeEnum::CAR}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, false, get_osr_parameters(query), query.pedestrianProfile_, query.elevationCosts_, max, query.maxMatchingDistance_, gbfs_rd, stats); std::erase_if(offsets, [&](n::routing::offset const& o) { return r.ride_sharing_bounds_ != nullptr && !r.ride_sharing_bounds_->contains( r.tt_->locations_.coordinates_[o.target_]); }); for (auto& o : offsets) { o.duration_ += kODMTransferBuffer; } rides.reserve(offsets.size() * 2); n::routing::get_starts( dir == osr::direction::kForward ? n::direction::kForward : n::direction::kBackward, tt, rtt, intvl, offsets, {}, {}, n::routing::kMaxTravelTime, location_match_mode, false, rides, true, start_time.prf_idx_, start_time.transfer_time_settings_); } void prima::init(n::interval const& search_intvl, n::interval const& taxi_intvl, bool use_first_mile_taxi, bool use_last_mile_taxi, bool use_direct_taxi, bool use_first_mile_ride_sharing, bool use_last_mile_ride_sharing, bool use_direct_ride_sharing, n::timetable const& tt, n::rt_timetable const* rtt, ep::routing const& r, elevators const* e, gbfs::gbfs_routing_data& gbfs, api::Place const& from, api::Place const& to, api::plan_params const& query, n::routing::query const& n_query, unsigned api_version) { direct_duration_ = std::optional{}; if ((use_direct_ride_sharing || use_direct_taxi) && r.w_ && r.l_ && (r.ride_sharing_bounds_ == nullptr || (r.ride_sharing_bounds_->contains(from_.pos_) && r.ride_sharing_bounds_->contains(to_.pos_)))) { direct_duration_ = init_direct(direct_ride_sharing_, r, e, gbfs, from, to, search_intvl, query, api_version); if (use_direct_taxi && r.odm_bounds_ != nullptr && r.odm_bounds_->contains(from_.pos_) && r.odm_bounds_->contains(to_.pos_)) { direct_taxi_ = direct_ride_sharing_; } if (!use_direct_ride_sharing) { direct_ride_sharing_.clear(); } } auto const max_offset_duration = direct_duration_ ? std::min(std::max(*direct_duration_, kODMOffsetMinImprovement) - kODMOffsetMinImprovement, kODMMaxDuration) : kODMMaxDuration; if (use_first_mile_ride_sharing || use_first_mile_taxi) { init_pt( first_mile_taxi_, first_mile_ride_sharing_, r, from_, osr::direction::kForward, query, gbfs, tt, rtt, taxi_intvl, n_query, query.arriveBy_ ? n_query.dest_match_mode_ : n_query.start_match_mode_, max_offset_duration); if (!use_first_mile_taxi || r.odm_bounds_ == nullptr || !r.odm_bounds_->contains(from_.pos_)) { first_mile_taxi_.clear(); } else { std::erase_if(first_mile_taxi_, [&](n::routing::offset const& o) { return !r.odm_bounds_->contains( r.tt_->locations_.coordinates_[o.target_]); }); } if (!use_first_mile_ride_sharing) { first_mile_ride_sharing_.clear(); } } if (use_last_mile_ride_sharing || use_last_mile_taxi) { init_pt( last_mile_taxi_, last_mile_ride_sharing_, r, to_, osr::direction::kBackward, query, gbfs, tt, rtt, taxi_intvl, n_query, query.arriveBy_ ? n_query.start_match_mode_ : n_query.dest_match_mode_, max_offset_duration); if (!use_last_mile_taxi || r.odm_bounds_ == nullptr || !r.odm_bounds_->contains(to_.pos_)) { last_mile_taxi_.clear(); } else { std::erase_if(last_mile_taxi_, [&](n::routing::offset const& o) { return !r.odm_bounds_->contains( r.tt_->locations_.coordinates_[o.target_]); }); } if (!use_last_mile_ride_sharing) { last_mile_ride_sharing_.clear(); } } auto const by_duration = [](auto const& a, auto const& b) { return a.duration_ < b.duration_; }; utl::sort(first_mile_taxi_, by_duration); utl::sort(last_mile_taxi_, by_duration); } std::int64_t to_millis(n::unixtime_t const t) { return std::chrono::duration_cast( t.time_since_epoch()) .count(); } n::unixtime_t to_unix(std::int64_t const t) { return n::unixtime_t{ std::chrono::duration_cast(std::chrono::milliseconds{t})}; } json::array to_json(std::vector const& rides, n::timetable const& tt, which_mile const wm) { auto a = json::array{}; utl::equal_ranges_linear( rides, [](n::routing::start const& a, n::routing::start const& b) { return a.stop_ == b.stop_; }, [&](auto&& from_it, auto&& to_it) { auto const& pos = tt.locations_.coordinates_[from_it->stop_]; a.emplace_back(json::value{ {"lat", pos.lat_}, {"lng", pos.lng_}, {"times", utl::all(from_it, to_it) | utl::transform([&](n::routing::start const& s) { return wm == kFirstMile ? to_millis(s.time_at_stop_ - kODMTransferBuffer) : to_millis(s.time_at_stop_ + kODMTransferBuffer); }) | utl::emplace_back_to()}}); }); return a; } json::array to_json(std::vector const& v, n::event_type const fixed) { return utl::all(v) // | utl::transform([&](direct_ride const& r) { return to_millis(fixed == n::event_type::kDep ? r.dep_ : r.arr_); }) // | utl::emplace_back_to(); } void tag_invoke(json::value_from_tag const&, json::value& jv, capacities const& c) { jv = {{"wheelchairs", c.wheelchairs_}, {"bikes", c.bikes_}, {"passengers", c.passengers_}, {"luggage", c.luggage_}}; } std::string make_whitelist_request( osr::location const& from, osr::location const& to, std::vector const& first_mile, std::vector const& last_mile, std::vector const& direct, n::event_type const fixed, capacities const& cap, n::timetable const& tt) { return json::serialize( json::value{{"start", {{"lat", from.pos_.lat_}, {"lng", from.pos_.lng_}}}, {"target", {{"lat", to.pos_.lat_}, {"lng", to.pos_.lng_}}}, {"startBusStops", to_json(first_mile, tt, kFirstMile)}, {"targetBusStops", to_json(last_mile, tt, kLastMile)}, {"directTimes", to_json(direct, fixed)}, {"startFixed", fixed == n::event_type::kDep}, {"capacities", json::value_from(cap)}}); } std::size_t prima::n_ride_sharing_events() const { return first_mile_ride_sharing_.size() + last_mile_ride_sharing_.size() + direct_ride_sharing_.size(); } std::size_t n_rides_in_response(json::array const& ja) { return std::accumulate( ja.begin(), ja.end(), std::size_t{0U}, [](auto const& a, auto const& b) { return a + b.as_array().size(); }); } void fix_first_mile_duration(std::vector& journeys, std::vector const& first_mile, std::vector const& prev_first_mile, n::transport_mode_id_t const mode) { for (auto const [curr, prev] : utl::zip(first_mile, prev_first_mile)) { auto const uses_prev = [&, prev2 = prev /* hack for MacOS - fixed with 16 */]( n::routing::journey const& j) { return j.legs_.size() > 1 && j.legs_.front().dep_time_ == prev2.time_at_start_ && j.legs_.front().arr_time_ >= prev2.time_at_stop_ && (j.legs_.front().arr_time_ == prev2.time_at_stop_ || mode == kRideSharingTransportModeId) && j.legs_.front().to_ == prev2.stop_ && is_odm_leg(j.legs_.front(), mode); }; if (curr.time_at_start_ == kInfeasible) { utl::erase_if(journeys, uses_prev); } else { for (auto& j : journeys) { if (uses_prev(j)) { auto const l = begin(j.legs_); if (std::holds_alternative(std::next(l)->uses_)) { continue; // odm leg fixed already before with a different // time_at_stop (rideshare) } l->dep_time_ = curr.time_at_start_; l->arr_time_ = curr.time_at_stop_ - (mode == kRideSharingTransportModeId ? kODMTransferBuffer : n::duration_t{0}); std::get(l->uses_).duration_ = l->arr_time_ - l->dep_time_; // fill gap (transfer/waiting) with footpath j.legs_.emplace( std::next(l), n::direction::kForward, l->to_, l->to_, l->arr_time_, std::next(l)->dep_time_, n::footpath{l->to_, std::next(l)->dep_time_ - l->arr_time_}); } } } } }; void fix_last_mile_duration(std::vector& journeys, std::vector const& last_mile, std::vector const& prev_last_mile, n::transport_mode_id_t const mode) { for (auto const [curr, prev] : utl::zip(last_mile, prev_last_mile)) { auto const uses_prev = [&, prev2 = prev /* hack for MacOS - fixed with 16 */](auto const& j) { return j.legs_.size() > 1 && j.legs_.back().dep_time_ <= prev2.time_at_stop_ && (j.legs_.back().dep_time_ == prev2.time_at_stop_ || mode == kRideSharingTransportModeId) && j.legs_.back().arr_time_ == prev2.time_at_start_ && j.legs_.back().from_ == prev2.stop_ && is_odm_leg(j.legs_.back(), mode); }; if (curr.time_at_start_ == kInfeasible) { utl::erase_if(journeys, uses_prev); } else { for (auto& j : journeys) { if (uses_prev(j)) { auto const l = std::prev(end(j.legs_)); if (std::holds_alternative(std::prev(l)->uses_)) { continue; // odm leg fixed already before with a different // time_at_stop (rideshare) } l->dep_time_ = curr.time_at_stop_ + (mode == kRideSharingTransportModeId ? kODMTransferBuffer : n::duration_t{0}); l->arr_time_ = curr.time_at_start_; std::get(l->uses_).duration_ = l->arr_time_ - l->dep_time_; // fill gap (transfer/waiting) with footpath j.legs_.emplace( l, n::direction::kForward, l->from_, l->from_, std::prev(l)->arr_time_, l->dep_time_, n::footpath{l->from_, l->dep_time_ - std::prev(l)->arr_time_}); } } } } }; void add_direct_odm(std::vector const& direct, std::vector& odm_journeys, place_t const& from, place_t const& to, bool arrive_by, n::transport_mode_id_t const mode) { auto from_l = std::visit( utl::overloaded{[](osr::location const&) { return get_special_station(n::special_station::kStart); }, [](tt_location const& tt_l) { return tt_l.l_; }}, from); auto to_l = std::visit( utl::overloaded{[](osr::location const&) { return get_special_station(n::special_station::kEnd); }, [](tt_location const& tt_l) { return tt_l.l_; }}, to); if (arrive_by) { std::swap(from_l, to_l); } for (auto const& d : direct) { odm_journeys.push_back(n::routing::journey{ .legs_ = {{n::direction::kForward, from_l, to_l, d.dep_, d.arr_, n::routing::offset{to_l, std::chrono::abs(d.arr_ - d.dep_), mode}}}, .start_time_ = d.dep_, .dest_time_ = d.arr_, .dest_ = to_l, .transfers_ = 0U}); } n::log(n::log_lvl::debug, "motis.prima", "[whitelist] added {} direct rides for mode {}", direct.size(), mode); } } // namespace motis::odm ================================================ FILE: src/odm/query_factory.cc ================================================ #include "motis/odm/query_factory.h" #include "motis/endpoints/routing.h" namespace motis::odm { namespace n = nigiri; std::vector query_factory::make_queries( bool const with_taxi, bool const with_ride_sharing) const { auto queries = std::vector{}; queries.push_back( make(start_walk_, td_start_walk_, dest_walk_, td_dest_walk_)); if (with_taxi) { if (!dest_taxi_short_.empty()) { queries.push_back( make(start_walk_, td_start_walk_, dest_walk_, dest_taxi_short_)); } if (!dest_taxi_long_.empty()) { queries.push_back( make(start_walk_, td_start_walk_, dest_walk_, dest_taxi_long_)); } if (!start_taxi_short_.empty()) { queries.push_back( make(start_walk_, start_taxi_short_, dest_walk_, td_dest_walk_)); } if (!start_taxi_long_.empty()) { queries.push_back( make(start_walk_, start_taxi_long_, dest_walk_, td_dest_walk_)); } } if (with_ride_sharing) { if (!start_ride_sharing_.empty()) { queries.push_back( make(start_walk_, start_ride_sharing_, dest_walk_, td_dest_walk_)); } if (!dest_ride_sharing_.empty()) { queries.push_back( make(start_walk_, td_start_walk_, dest_walk_, dest_ride_sharing_)); } } return queries; } n::routing::query query_factory::make( std::vector const& start, n::hash_map> const& td_start, std::vector const& dest, n::hash_map> const& td_dest) const { auto q = base_query_; q.start_ = start; q.destination_ = dest; q.td_start_ = td_start; q.td_dest_ = td_dest; motis::ep::remove_slower_than_fastest_direct(q); return q; } } // namespace motis::odm ================================================ FILE: src/odm/shorten.cc ================================================ #include "motis/odm/odm.h" #include "nigiri/for_each_meta.h" #include "nigiri/logging.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/timetable.h" #include "motis-api/motis-api.h" #include "motis/odm/prima.h" #include "motis/transport_mode_ids.h" using namespace std::chrono_literals; namespace n = nigiri; namespace nr = nigiri::routing; namespace motis::odm { void shorten(std::vector& odm_journeys, std::vector const& first_mile_taxi, std::vector const& first_mile_taxi_times, std::vector const& last_mile_taxi, std::vector const& last_mile_taxi_times, n::timetable const& tt, n::rt_timetable const* rtt, api::plan_params const& query) { auto const shorten_first_leg = [&](nr::journey& j) { auto& odm_leg = begin(j.legs_)[0]; auto& pt_leg = begin(j.legs_)[1]; if (!is_odm_leg(odm_leg, kOdmTransportModeId) || !std::holds_alternative(pt_leg.uses_)) { return; } auto& ree = std::get(pt_leg.uses_); auto run = n::rt::frun(tt, rtt, ree.r_); run.stop_range_.to_ = ree.stop_range_.to_ - 1U; auto min_stop_idx = ree.stop_range_.from_; auto min_odm_duration = odm_time(odm_leg); auto shorter_ride = std::optional{}; for (auto const stop : run) { if (stop.is_cancelled() || !stop.in_allowed(query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR) || (query.requireBikeTransport_ && !stop.bikes_allowed(n::event_type::kDep)) || (query.requireCarTransport_ && !stop.cars_allowed(n::event_type::kDep))) { continue; } for (auto const [offset, times] : utl::zip(first_mile_taxi, first_mile_taxi_times)) { if (nr::matches(tt, nr::location_match_mode::kExact, offset.target_, stop.get_location_idx()) && utl::any_of(times, [&](auto const& t) { return t.contains(stop.time(n::event_type::kDep) - offset.duration_) && t.contains(stop.time(n::event_type::kDep) - 1min); })) { if (offset.duration_ < min_odm_duration) { min_stop_idx = stop.stop_idx_; min_odm_duration = offset.duration_; shorter_ride = {.time_at_start_ = stop.time(n::event_type::kDep) - offset.duration_, .time_at_stop_ = stop.time(n::event_type::kDep), .stop_ = offset.target_}; } break; } } } if (shorter_ride) { auto& odm_offset = std::get(odm_leg.uses_); auto const old_stop = odm_leg.to_; auto const old_odm_time = std::chrono::minutes{odm_offset.duration_}; auto const old_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_; j.start_time_ = odm_leg.dep_time_ = shorter_ride->time_at_start_; odm_offset.duration_ = min_odm_duration; odm_leg.arr_time_ = pt_leg.dep_time_ = shorter_ride->time_at_stop_; odm_leg.to_ = odm_offset.target_ = pt_leg.from_ = shorter_ride->stop_; ree.stop_range_.from_ = min_stop_idx; auto const new_stop = odm_leg.to_; auto const new_odm_time = std::chrono::minutes{odm_offset.duration_}; auto const new_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_; n::log(n::log_lvl::debug, "motis.prima", "shorten first leg: [stop: {}, ODM: {}, PT: {}] -> [stop: {}, " "ODM: {}, PT: {}] (ODM: -{}, PT: +{})", n::loc{tt, old_stop}, old_odm_time, old_pt_time, n::loc{tt, new_stop}, new_odm_time, new_pt_time, std::chrono::minutes{old_odm_time - new_odm_time}, new_pt_time - old_pt_time); } }; auto const shorten_last_leg = [&](nr::journey& j) { auto& odm_leg = rbegin(j.legs_)[0]; auto& pt_leg = rbegin(j.legs_)[1]; if (!is_odm_leg(odm_leg, kOdmTransportModeId) || !std::holds_alternative(pt_leg.uses_)) { return; } auto& ree = std::get(pt_leg.uses_); auto run = n::rt::frun(tt, rtt, ree.r_); run.stop_range_.from_ = ree.stop_range_.from_ + 1U; auto min_stop_idx = static_cast(ree.stop_range_.to_ - 1U); auto min_odm_duration = odm_time(odm_leg); auto shorter_ride = std::optional{}; for (auto const stop : run) { if (stop.is_cancelled() || !stop.out_allowed(query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR) || (query.requireBikeTransport_ && !stop.bikes_allowed(n::event_type::kArr)) || (query.requireCarTransport_ && !stop.cars_allowed(n::event_type::kArr))) { continue; } for (auto const [offset, times] : utl::zip(last_mile_taxi, last_mile_taxi_times)) { if (nr::matches(tt, nr::location_match_mode::kExact, offset.target_, stop.get_location_idx()) && utl::any_of(times, [&](auto const& t) { return t.contains(stop.time(n::event_type::kArr)) && t.contains(stop.time(n::event_type::kArr) + offset.duration_ - 1min); })) { if (offset.duration_ < min_odm_duration) { min_stop_idx = stop.stop_idx_; min_odm_duration = offset.duration_; shorter_ride = {.time_at_start_ = stop.time(n::event_type::kArr) + offset.duration_, .time_at_stop_ = stop.time(n::event_type::kArr), .stop_ = offset.target_}; } break; } } } if (shorter_ride) { auto& odm_offset = std::get(odm_leg.uses_); auto const old_stop = odm_leg.from_; auto const old_odm_time = std::chrono::minutes{odm_offset.duration_}; auto const old_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_; ree.stop_range_.to_ = min_stop_idx + 1U; pt_leg.to_ = odm_leg.from_ = odm_offset.target_ = shorter_ride->stop_; pt_leg.arr_time_ = odm_leg.dep_time_ = shorter_ride->time_at_stop_; odm_offset.duration_ = min_odm_duration; j.dest_time_ = odm_leg.arr_time_ = shorter_ride->time_at_start_; auto const new_stop = odm_leg.from_; auto const new_odm_time = std::chrono::minutes{odm_offset.duration_}; auto const new_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_; n::log(n::log_lvl::debug, "motis.prima", "shorten last leg: [stop: {}, ODM: {}, PT: {}] -> [stop: {}, " "ODM: {}, PT: {}] (ODM: -{}, PT: +{})", n::loc{tt, old_stop}, old_odm_time, old_pt_time, n::loc{tt, new_stop}, new_odm_time, new_pt_time, std::chrono::minutes{old_odm_time - new_odm_time}, new_pt_time - old_pt_time); } }; for (auto& j : odm_journeys) { if (j.legs_.empty()) { n::log(n::log_lvl::debug, "motis.prima", "shorten: journey without legs"); continue; } shorten_first_leg(j); shorten_last_leg(j); } } } // namespace motis::odm ================================================ FILE: src/odm/td_offsets.cc ================================================ #include "motis/odm/td_offsets.h" #include using namespace std::chrono_literals; namespace n = nigiri; namespace nr = nigiri::routing; namespace motis::odm { std::pair get_td_offsets_split( std::vector const& offsets, std::vector const& times, n::transport_mode_id_t const mode) { auto const split = offsets.empty() ? 0 : std::distance(begin(offsets), std::upper_bound(begin(offsets), end(offsets), offsets[offsets.size() / 2], [](auto const& a, auto const& b) { return a.duration_ < b.duration_; })); auto const offsets_lo = offsets | std::views::take(split); auto const times_lo = times | std::views::take(split); auto const offsets_hi = offsets | std::views::drop(split); auto const times_hi = times | std::views::drop(split); auto const derive_td_offsets = [&](auto const& offsets_split, auto const& times_split) { auto td_offsets = nr::td_offsets_t{}; for (auto const [o, t] : std::views::zip(offsets_split, times_split)) { td_offsets.emplace(o.target_, std::vector{}); for (auto const& i : t) { td_offsets[o.target_].emplace_back(i.from_, o.duration_, mode); td_offsets[o.target_].emplace_back(i.to_ - o.duration_ + 1min, n::footpath::kMaxDuration, mode); } } return td_offsets; }; return std::pair{derive_td_offsets(offsets_lo, times_lo), derive_td_offsets(offsets_hi, times_hi)}; } } // namespace motis::odm ================================================ FILE: src/odm/whitelist_ridesharing.cc ================================================ #include "motis/odm/prima.h" #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/asio/io_context.hpp" #include "boost/json.hpp" #include "motis/http_req.h" #include "motis/odm/odm.h" namespace n = nigiri; namespace nr = nigiri::routing; namespace json = boost::json; using namespace std::chrono_literals; namespace motis::odm { std::string prima::make_ride_sharing_request(n::timetable const& tt) const { return make_whitelist_request(from_, to_, first_mile_ride_sharing_, last_mile_ride_sharing_, direct_ride_sharing_, fixed_, cap_, tt); } bool prima::consume_ride_sharing_response(std::string_view json) { auto const update_first_mile = [&](json::array const& update) { auto const n = n_rides_in_response(update); if (first_mile_ride_sharing_.size() != n) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] first mile ride-sharing #rides != #updates ({} " "!= {})", first_mile_ride_sharing_.size(), n); first_mile_ride_sharing_.clear(); return true; } auto prev_first_mile = std::exchange(first_mile_ride_sharing_, std::vector{}); auto prev_it = std::begin(prev_first_mile); for (auto const& stop : update) { for (auto const& time : stop.as_array()) { if (!time.is_null() && time.is_array()) { for (auto const& event : time.as_array()) { first_mile_ride_sharing_.push_back( {.time_at_start_ = to_unix(event.as_object().at("pickupTime").as_int64()), .time_at_stop_ = to_unix(event.as_object().at("dropoffTime").as_int64()) + kODMTransferBuffer, .stop_ = prev_it->stop_}); first_mile_ride_sharing_tour_ids_.emplace_back( event.as_object().at("tripId").as_string()); } } ++prev_it; } } return false; }; auto const update_last_mile = [&](json::array const& update) { auto const n = n_rides_in_response(update); if (last_mile_ride_sharing_.size() != n) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] last mile ride-sharing #rides != #updates ({} " "!= {})", last_mile_ride_sharing_.size(), n); last_mile_ride_sharing_.clear(); return true; } auto prev_last_mile = std::exchange(last_mile_ride_sharing_, std::vector{}); auto prev_it = std::begin(prev_last_mile); for (auto const& stop : update) { for (auto const& time : stop.as_array()) { if (!time.is_null() && time.is_array()) { for (auto const& event : time.as_array()) { last_mile_ride_sharing_.push_back( {.time_at_start_ = to_unix(event.as_object().at("dropoffTime").as_int64()), .time_at_stop_ = to_unix(event.as_object().at("pickupTime").as_int64()) - kODMTransferBuffer, .stop_ = prev_it->stop_}); last_mile_ride_sharing_tour_ids_.emplace_back( event.as_object().at("tripId").as_string()); } ++prev_it; } } } return false; }; auto const update_direct = [&](json::array const& update) { if (direct_ride_sharing_.size() != update.size()) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride-sharing] direct ride-sharing #rides != " "#updates " "({} != {})", direct_ride_sharing_.size(), update.size()); direct_ride_sharing_.clear(); return true; } direct_ride_sharing_.clear(); for (auto const& time : update) { if (time.is_array()) { for (auto const& ride : time.as_array()) { if (!ride.is_null()) { direct_ride_sharing_.push_back( {to_unix(ride.as_object().at("pickupTime").as_int64()), to_unix(ride.as_object().at("dropoffTime").as_int64())}); direct_ride_sharing_tour_ids_.emplace_back( ride.as_object().at("tripId").as_string()); } } } } return false; }; auto with_errors = false; try { auto const o = json::parse(json).as_object(); with_errors |= update_first_mile(o.at("start").as_array()); with_errors |= update_last_mile(o.at("target").as_array()); with_errors |= update_direct(o.at("direct").as_array()); } catch (std::exception const&) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride-sharing] could not parse response: {}", json); return false; } if (with_errors) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride-sharing] parsed response with errors: {}", json); return false; } return true; } bool prima::whitelist_ride_sharing(n::timetable const& tt) { auto response = std::optional{}; auto ioc = boost::asio::io_context{}; try { n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride-sharing] request for {} events", n_ride_sharing_events()); boost::asio::co_spawn( ioc, [&]() -> boost::asio::awaitable { auto const prima_msg = co_await http_POST(ride_sharing_whitelist_, kReqHeaders, make_ride_sharing_request(tt), 30s); response = get_http_body(prima_msg); }, boost::asio::detached); ioc.run(); } catch (std::exception const& e) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride-sharing] networking failed: {}", e.what()); response = std::nullopt; } if (!response) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist ride share] failed, discarding ride share journeys"); return false; } return consume_ride_sharing_response(*response); } } // namespace motis::odm ================================================ FILE: src/odm/whitelist_taxi.cc ================================================ #include "motis/odm/prima.h" #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/asio/io_context.hpp" #include "boost/json.hpp" #include "utl/erase_duplicates.h" #include "motis/http_req.h" #include "motis/odm/odm.h" #include "motis/transport_mode_ids.h" namespace n = nigiri; namespace nr = nigiri::routing; namespace json = boost::json; using namespace std::chrono_literals; namespace motis::odm { std::string prima::make_whitelist_taxi_request( std::vector const& first_mile, std::vector const& last_mile, n::timetable const& tt) const { return make_whitelist_request(from_, to_, first_mile, last_mile, direct_taxi_, fixed_, cap_, tt); } void extract_taxis(std::vector const& journeys, std::vector& first_mile_taxi_rides, std::vector& last_mile_taxi_rides) { for (auto const& j : journeys) { if (!j.legs_.empty()) { if (is_odm_leg(j.legs_.front(), kOdmTransportModeId)) { first_mile_taxi_rides.push_back({ .time_at_start_ = j.legs_.front().dep_time_, .time_at_stop_ = j.legs_.front().arr_time_, .stop_ = j.legs_.front().to_, }); } } if (j.legs_.size() > 1) { if (is_odm_leg(j.legs_.back(), kOdmTransportModeId)) { last_mile_taxi_rides.push_back({ .time_at_start_ = j.legs_.back().arr_time_, .time_at_stop_ = j.legs_.back().dep_time_, .stop_ = j.legs_.back().from_, }); } } } utl::erase_duplicates(first_mile_taxi_rides, by_stop, std::equal_to<>{}); utl::erase_duplicates(last_mile_taxi_rides, by_stop, std::equal_to<>{}); } void prima::extract_taxis_for_persisting( std::vector const& journeys) { whitelist_first_mile_locations_.clear(); whitelist_last_mile_locations_.clear(); for (auto const& j : journeys) { if (j.legs_.size() <= 1) { continue; } if (is_odm_leg(j.legs_.front(), kOdmTransportModeId)) { whitelist_first_mile_locations_.push_back(j.legs_.front().to_); } if (is_odm_leg(j.legs_.back(), kOdmTransportModeId)) { whitelist_last_mile_locations_.push_back(j.legs_.back().from_); } } utl::erase_duplicates(whitelist_first_mile_locations_, std::less<>{}, std::equal_to<>{}); utl::erase_duplicates(whitelist_last_mile_locations_, std::less<>{}, std::equal_to<>{}); } bool prima::consume_whitelist_taxi_response( std::string_view json, std::vector& journeys, std::vector& first_mile_taxi_rides, std::vector& last_mile_taxi_rides) { auto const update_first_mile = [&](json::array const& update) { auto const n_pt_udpates = n_rides_in_response(update); if (first_mile_taxi_rides.size() != n_pt_udpates) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] first mile taxi #rides != #updates ({} != {})", first_mile_taxi_rides.size(), n_pt_udpates); return true; } auto const prev_first_mile = std::exchange(first_mile_taxi_rides, std::vector{}); auto prev_it = std::begin(prev_first_mile); for (auto const& stop : update) { for (auto const& event : stop.as_array()) { if (event.is_null()) { first_mile_taxi_rides.push_back({.time_at_start_ = kInfeasible, .time_at_stop_ = kInfeasible, .stop_ = prev_it->stop_}); } else { first_mile_taxi_rides.push_back({ .time_at_start_ = to_unix(event.as_object().at("pickupTime").as_int64()), .time_at_stop_ = to_unix(event.as_object().at("dropoffTime").as_int64()), .stop_ = prev_it->stop_, }); } ++prev_it; } } fix_first_mile_duration(journeys, first_mile_taxi_rides, prev_first_mile, kOdmTransportModeId); return false; }; auto const update_last_mile = [&](json::array const& update) { auto const n_pt_udpates = n_rides_in_response(update); if (last_mile_taxi_rides.size() != n_pt_udpates) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] last mile taxi #rides != #updates ({} != {})", last_mile_taxi_rides.size(), n_pt_udpates); return true; } auto const prev_last_mile = std::exchange(last_mile_taxi_rides, std::vector{}); auto prev_it = std::begin(prev_last_mile); for (auto const& stop : update) { for (auto const& event : stop.as_array()) { if (event.is_null()) { last_mile_taxi_rides.push_back({ .time_at_start_ = kInfeasible, .time_at_stop_ = kInfeasible, .stop_ = prev_it->stop_, }); } else { last_mile_taxi_rides.push_back({ .time_at_start_ = to_unix(event.as_object().at("dropoffTime").as_int64()), .time_at_stop_ = to_unix(event.as_object().at("pickupTime").as_int64()), .stop_ = prev_it->stop_, }); } ++prev_it; } } fix_last_mile_duration(journeys, last_mile_taxi_rides, prev_last_mile, kOdmTransportModeId); return false; }; auto const update_direct_rides = [&](json::array const& update) { if (direct_taxi_.size() != update.size()) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] direct taxi #rides != #updates ({} != {})", direct_taxi_.size(), update.size()); direct_taxi_.clear(); return true; } direct_taxi_.clear(); for (auto const& ride : update) { if (!ride.is_null()) { direct_taxi_.push_back( {to_unix(ride.as_object().at("pickupTime").as_int64()), to_unix(ride.as_object().at("dropoffTime").as_int64())}); } } return false; }; auto with_errors = false; try { auto const o = json::parse(json).as_object(); with_errors |= update_first_mile(o.at("start").as_array()); with_errors |= update_last_mile(o.at("target").as_array()); with_errors |= update_direct_rides(o.at("direct").as_array()); } catch (std::exception const&) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] could not parse response: {}", json); return false; } if (with_errors) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] parsed response with errors: {}", json); return false; } // adjust journey start/dest times after adjusting legs for (auto& j : journeys) { if (!j.legs_.empty()) { j.start_time_ = j.legs_.front().dep_time_; j.dest_time_ = j.legs_.back().arr_time_; } } return true; } bool prima::whitelist_taxi(std::vector& taxi_journeys, n::timetable const& tt) { auto first_mile_taxi_rides = std::vector{}; auto last_mile_taxi_rides = std::vector{}; extract_taxis(taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides); extract_taxis_for_persisting(taxi_journeys); auto whitelist_response = std::optional{}; auto ioc = boost::asio::io_context{}; try { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] request for {} rides", first_mile_taxi_rides.size() + last_mile_taxi_rides.size() + direct_taxi_.size()); boost::asio::co_spawn( ioc, [&]() -> boost::asio::awaitable { auto const prima_msg = co_await http_POST( taxi_whitelist_, kReqHeaders, make_whitelist_request(from_, to_, first_mile_taxi_rides, last_mile_taxi_rides, direct_taxi_, fixed_, cap_, tt), 10s); whitelist_response = get_http_body(prima_msg); }, boost::asio::detached); ioc.run(); } catch (std::exception const& e) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] networking failed: {}", e.what()); whitelist_response = std::nullopt; } if (!whitelist_response) { n::log(n::log_lvl::debug, "motis.prima", "[whitelist taxi] failed, discarding taxi journeys"); return false; } auto const was_whitelist_response_valid = consume_whitelist_taxi_response( *whitelist_response, taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides); if (!was_whitelist_response_valid) { return false; } whitelist_response_ = json::parse(whitelist_response.value()).as_object(); return true; } } // namespace motis::odm ================================================ FILE: src/osr/max_distance.cc ================================================ #include "motis/osr/max_distance.h" #include namespace motis { double get_max_distance(osr::search_profile const profile, std::chrono::seconds const t) { auto seconds = static_cast(t.count()); switch (profile) { case osr::search_profile::kWheelchair: return seconds * 0.8; case osr::search_profile::kFoot: return seconds * 1.1; case osr::search_profile::kBikeSharing: [[fallthrough]]; case osr::search_profile::kBikeElevationLow: [[fallthrough]]; case osr::search_profile::kBikeElevationHigh: [[fallthrough]]; case osr::search_profile::kBikeFast: [[fallthrough]]; case osr::search_profile::kBike: return seconds * 4.0; case osr::search_profile::kCar: [[fallthrough]]; case osr::search_profile::kCarDropOff: [[fallthrough]]; case osr::search_profile::kCarDropOffWheelchair: [[fallthrough]]; case osr::search_profile::kCarSharing: [[fallthrough]]; case osr::search_profile::kCarParking: [[fallthrough]]; case osr::search_profile::kCarParkingWheelchair: return seconds * 28.0; case osr::search_profile::kBus: return seconds * 5.0; case osr::search_profile::kRailway: return seconds * 5.5; case osr::search_profile::kFerry: return seconds * 4.0; } std::unreachable(); } } // namespace motis ================================================ FILE: src/osr/mode_to_profile.cc ================================================ #include "motis/osr/mode_to_profile.h" #include "utl/verify.h" namespace motis { api::ModeEnum to_mode(osr::mode const m) { switch (m) { case osr::mode::kFoot: [[fallthrough]]; case osr::mode::kWheelchair: return api::ModeEnum::WALK; case osr::mode::kBike: return api::ModeEnum::BIKE; case osr::mode::kCar: return api::ModeEnum::CAR; case osr::mode::kRailway: return api::ModeEnum::DEBUG_RAILWAY_ROUTE; case osr::mode::kFerry: return api::ModeEnum::DEBUG_FERRY_ROUTE; } std::unreachable(); } osr::search_profile to_profile( api::ModeEnum const m, api::PedestrianProfileEnum const pedestrian_profile, api::ElevationCostsEnum const elevation_costs) { auto const wheelchair = pedestrian_profile == api::PedestrianProfileEnum::WHEELCHAIR; switch (m) { case api::ModeEnum::WALK: return wheelchair ? osr::search_profile::kWheelchair : osr::search_profile::kFoot; case api::ModeEnum::BIKE: switch (elevation_costs) { case api::ElevationCostsEnum::NONE: return osr::search_profile::kBike; case api::ElevationCostsEnum::LOW: return osr::search_profile::kBikeElevationLow; case api::ElevationCostsEnum::HIGH: return osr::search_profile::kBikeElevationHigh; } return osr::search_profile::kBike; // Fallback if invalid value is used case api::ModeEnum::ODM: [[fallthrough]]; case api::ModeEnum::RIDE_SHARING: [[fallthrough]]; case api::ModeEnum::CAR: return osr::search_profile::kCar; case api::ModeEnum::CAR_DROPOFF: return wheelchair ? osr::search_profile::kCarDropOffWheelchair : osr::search_profile::kCarDropOff; case api::ModeEnum::CAR_PARKING: return wheelchair ? osr::search_profile::kCarParkingWheelchair : osr::search_profile::kCarParking; case api::ModeEnum::RENTAL: // could be kBikeSharing or kCarSharing, use gbfs::get_osr_profile() // to get the correct profile for each product return osr::search_profile::kBikeSharing; case api::ModeEnum::DEBUG_BUS_ROUTE: return osr::search_profile::kBus; case api::ModeEnum::DEBUG_RAILWAY_ROUTE: return osr::search_profile::kRailway; case api::ModeEnum::DEBUG_FERRY_ROUTE: return osr::search_profile::kFerry; default: throw utl::fail("unsupported mode"); } } } // namespace motis ================================================ FILE: src/osr/parameters.cc ================================================ #include "motis/osr/parameters.h" #include #include #include "utl/verify.h" #include "osr/routing/profiles/bike.h" #include "osr/routing/profiles/bike_sharing.h" #include "osr/routing/profiles/car.h" #include "osr/routing/profiles/car_parking.h" #include "osr/routing/profiles/car_sharing.h" #include "osr/routing/profiles/foot.h" namespace motis { template concept HasPedestrianProfile = requires(T const& params) { params.pedestrianProfile_; }; template concept HasPedestrianProfileAndSpeed = HasPedestrianProfile && std::is_same_v().pedestrianSpeed_), std::optional>; template bool use_wheelchair(T const&) { return false; } template bool use_wheelchair(T const& t) requires HasPedestrianProfile { return t.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR; } template float pedestrian_speed(T const&) { return osr_parameters::kFootSpeed; } template <> float pedestrian_speed(api::PedestrianProfileEnum const& p) { return p == api::PedestrianProfileEnum::FOOT ? osr_parameters::kFootSpeed : osr_parameters::kWheelchairSpeed; } template float pedestrian_speed(T const& params) requires HasPedestrianProfile { return pedestrian_speed(params.pedestrianProfile_); } template float pedestrian_speed(T const& params) requires HasPedestrianProfileAndSpeed { return params.pedestrianSpeed_ .and_then([](auto const speed) { return speed > 0.3 && speed < 10.0 ? std::optional{static_cast(speed)} : std::nullopt; }) .value_or(pedestrian_speed(params.pedestrianProfile_)); } template float cycling_speed(T const&) { return osr_parameters::kBikeSpeed; } template float cycling_speed(T const& params) requires( std::is_same_v>) { return params.cyclingSpeed_ .and_then([](auto const speed) { return speed > 0.7 && speed < 20.0 ? std::optional{static_cast(speed)} : std::nullopt; }) .value_or(osr_parameters::kBikeSpeed); } template osr_parameters to_osr_parameters(T const& params) { return { .pedestrian_speed_ = pedestrian_speed(params), .cycling_speed_ = cycling_speed(params), .use_wheelchair_ = use_wheelchair(params), }; } osr_parameters get_osr_parameters(api::plan_params const& params) { return to_osr_parameters(params); } osr_parameters get_osr_parameters(api::oneToAll_params const& params) { return to_osr_parameters(params); } osr_parameters get_osr_parameters(api::oneToMany_params const& params) { return to_osr_parameters(params); } osr_parameters get_osr_parameters(api::OneToManyParams const& params) { return to_osr_parameters(params); } osr_parameters get_osr_parameters( api::oneToManyIntermodal_params const& params) { return to_osr_parameters(params); } osr_parameters get_osr_parameters( api::OneToManyIntermodalParams const& params) { return to_osr_parameters(params); } osr::profile_parameters to_profile_parameters(osr::search_profile const p, osr_parameters const& params) { // Ensure correct speed is used when using default parameters auto const wheelchair_speed = params.use_wheelchair_ ? params.pedestrian_speed_ : osr_parameters::kWheelchairSpeed; switch (p) { case osr::search_profile::kFoot: return osr::foot::parameters{ .speed_meters_per_second_ = params.pedestrian_speed_}; case osr::search_profile::kWheelchair: return osr::foot::parameters{ .speed_meters_per_second_ = wheelchair_speed}; case osr::search_profile::kBike: return osr::bike::parameters{ .speed_meters_per_second_ = params.cycling_speed_}; case osr::search_profile::kBikeFast: return osr::bike::parameters{ .speed_meters_per_second_ = params.cycling_speed_}; case osr::search_profile::kBikeElevationLow: return osr::bike::parameters{ .speed_meters_per_second_ = params.cycling_speed_}; case osr::search_profile::kBikeElevationHigh: return osr::bike::parameters{ .speed_meters_per_second_ = params.cycling_speed_}; case osr::search_profile::kCar: return osr::car::parameters{}; case osr::search_profile::kCarDropOff: return osr::car_parking::parameters{ .car_ = {}, .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}}; case osr::search_profile::kCarDropOffWheelchair: return osr::car_parking::parameters{ .car_ = {}, .foot_ = {.speed_meters_per_second_ = wheelchair_speed}}; case osr::search_profile::kCarParking: return osr::car_parking::parameters{ .car_ = {}, .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}}; case osr::search_profile::kCarParkingWheelchair: return osr::car_parking::parameters{ .car_ = {}, .foot_ = {.speed_meters_per_second_ = wheelchair_speed}}; case osr::search_profile::kBikeSharing: return osr::bike_sharing::parameters{ .bike_ = {.speed_meters_per_second_ = params.cycling_speed_}, .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}}; case osr::search_profile::kCarSharing: return osr::car_sharing::parameters{ .car_ = {}, .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}}; case osr::search_profile::kBus: return osr::bus::parameters{}; case osr::search_profile::kRailway: return osr::railway::parameters{}; case osr::search_profile::kFerry: return osr::ferry::parameters{}; } throw utl::fail("{} is not a valid profile", static_cast(p)); } } // namespace motis ================================================ FILE: src/osr/street_routing.cc ================================================ #include "motis/osr/street_routing.h" #include "geo/polyline_format.h" #include "utl/concat.h" #include "utl/get_or_create.h" #include "osr/routing/algorithms.h" #include "osr/routing/parameters.h" #include "osr/routing/route.h" #include "osr/routing/sharing_data.h" #include "motis/constants.h" #include "motis/osr/mode_to_profile.h" #include "motis/place.h" #include "motis/polyline.h" #include "motis/transport_mode_ids.h" #include "motis/update_rtt_td_footpaths.h" #include "utl/verify.h" namespace n = nigiri; namespace motis { default_output::default_output(osr::ways const& w, osr::search_profile const profile) : w_{w}, profile_{profile}, id_{static_cast>(profile)} {} default_output::default_output(osr::ways const& w, nigiri::transport_mode_id_t const id) : w_{w}, profile_{id == kOdmTransportModeId || id == kRideSharingTransportModeId ? osr::search_profile::kCar : osr::search_profile{static_cast< std::underlying_type_t>(id)}}, id_{id} { utl::verify(id <= kRideSharingTransportModeId, "invalid mode id={}", id); } default_output::~default_output() = default; api::ModeEnum default_output::get_mode() const { if (id_ == kOdmTransportModeId) { return api::ModeEnum::ODM; } if (id_ == kRideSharingTransportModeId) { return api::ModeEnum::RIDE_SHARING; } switch (profile_) { case osr::search_profile::kFoot: [[fallthrough]]; case osr::search_profile::kWheelchair: return api::ModeEnum::WALK; case osr::search_profile::kBike: [[fallthrough]]; case osr::search_profile::kBikeFast: [[fallthrough]]; case osr::search_profile::kBikeElevationLow: [[fallthrough]]; case osr::search_profile::kBikeElevationHigh: return api::ModeEnum::BIKE; case osr::search_profile::kCar: return api::ModeEnum::CAR; case osr::search_profile::kCarParking: [[fallthrough]]; case osr::search_profile::kCarParkingWheelchair: return api::ModeEnum::CAR_PARKING; case osr::search_profile::kCarDropOff: [[fallthrough]]; case osr::search_profile::kCarDropOffWheelchair: return api::ModeEnum::CAR_DROPOFF; case osr::search_profile::kBikeSharing: [[fallthrough]]; case osr::search_profile::kCarSharing: return api::ModeEnum::RENTAL; case osr::search_profile::kBus: return api::ModeEnum::DEBUG_BUS_ROUTE; case osr::search_profile::kRailway: return api::ModeEnum::DEBUG_RAILWAY_ROUTE; case osr::search_profile::kFerry: return api::ModeEnum::DEBUG_FERRY_ROUTE; } return api::ModeEnum::OTHER; } osr::search_profile default_output::get_profile() const { return profile_; } api::Place default_output::get_place( nigiri::lang_t const&, osr::node_idx_t const n, std::optional const& tz) const { auto const pos = w_.get_node_pos(n).as_latlng(); return api::Place{.lat_ = pos.lat_, .lon_ = pos.lng_, .tz_ = tz, .vertexType_ = api::VertexTypeEnum::NORMAL}; } bool default_output::is_time_dependent() const { return profile_ == osr::search_profile::kWheelchair || profile_ == osr::search_profile::kCarParkingWheelchair || profile_ == osr::search_profile::kCarDropOffWheelchair; } transport_mode_t default_output::get_cache_key() const { return static_cast(profile_); } osr::sharing_data const* default_output::get_sharing_data() const { return nullptr; } void default_output::annotate_leg(n::lang_t const&, osr::node_idx_t, osr::node_idx_t, api::Leg&) const {} std::vector get_step_instructions( osr::ways const& w, osr::elevation_storage const* elevations, osr::location const& from, osr::location const& to, std::span segments, unsigned const api_version) { auto steps = std::vector{}; auto pred_lvl = from.lvl_.to_float(); for (auto const& s : segments) { if (s.from_ != osr::node_idx_t::invalid() && s.from_ < w.n_nodes() && w.r_->node_properties_[s.from_].is_elevator()) { steps.push_back(api::StepInstruction{ .relativeDirection_ = api::DirectionEnum::ELEVATOR, .fromLevel_ = pred_lvl, .toLevel_ = s.from_level_.to_float()}); } auto const way_name = s.way_ == osr::way_idx_t::invalid() ? osr::string_idx_t::invalid() : w.way_names_[s.way_]; auto const props = s.way_ != osr::way_idx_t::invalid() ? w.r_->way_properties_[s.way_] : osr::way_properties{}; steps.push_back(api::StepInstruction{ .relativeDirection_ = s.way_ != osr::way_idx_t::invalid() ? (props.is_elevator() ? api::DirectionEnum::ELEVATOR : props.is_steps() ? api::DirectionEnum::STAIRS : api::DirectionEnum::CONTINUE) : api::DirectionEnum::CONTINUE, // TODO entry/exit/u-turn .distance_ = static_cast(s.dist_), .fromLevel_ = s.from_level_.to_float(), .toLevel_ = s.to_level_.to_float(), .osmWay_ = s.way_ == osr::way_idx_t ::invalid() ? std::nullopt : std::optional{static_cast( to_idx(w.way_osm_idx_[s.way_]))}, .polyline_ = api_version == 1 ? to_polyline<7>(s.polyline_) : to_polyline<6>(s.polyline_), .streetName_ = way_name == osr::string_idx_t::invalid() ? "" : std::string{w.strings_[way_name].view()}, .exit_ = {}, // TODO .stayOn_ = false, // TODO .area_ = false, // TODO .toll_ = props.has_toll(), .accessRestriction_ = w.get_access_restriction(s.way_).and_then( [](std::string_view s) { return std::optional{std::string{s}}; }), .elevationUp_ = elevations ? std::optional{to_idx(s.elevation_.up_)} : std::nullopt, .elevationDown_ = elevations ? std::optional{to_idx(s.elevation_.down_)} : std::nullopt}); } if (!segments.empty()) { auto& last = segments.back(); if (last.to_ != osr::node_idx_t::invalid() && last.to_ < w.n_nodes() && w.r_->node_properties_[last.to_].is_elevator()) { steps.push_back(api::StepInstruction{ .relativeDirection_ = api::DirectionEnum::ELEVATOR, .fromLevel_ = pred_lvl, .toLevel_ = to.lvl_.to_float()}); } } return steps; } api::Itinerary dummy_itinerary(api::Place const& from, api::Place const& to, api::ModeEnum const mode, n::unixtime_t const start_time, n::unixtime_t const end_time) { auto itinerary = api::Itinerary{ .duration_ = std::chrono::duration_cast(end_time - start_time) .count(), .startTime_ = start_time, .endTime_ = end_time}; auto& leg = itinerary.legs_.emplace_back(api::Leg{ .mode_ = mode, .from_ = from, .to_ = to, .duration_ = std::chrono::duration_cast(end_time - start_time) .count(), .startTime_ = start_time, .endTime_ = end_time, .scheduledStartTime_ = start_time, .scheduledEndTime_ = end_time, .legGeometry_ = empty_polyline()}); leg.from_.pickupType_ = std::nullopt; leg.from_.dropoffType_ = std::nullopt; leg.to_.pickupType_ = std::nullopt; leg.to_.dropoffType_ = std::nullopt; leg.from_.departure_ = leg.from_.scheduledDeparture_ = leg.startTime_; leg.to_.arrival_ = leg.to_.scheduledArrival_ = leg.endTime_; return itinerary; } api::Itinerary street_routing(osr::ways const& w, osr::lookup const& l, elevators const* e, osr::elevation_storage const* elevations, n::lang_t const& lang, api::Place const& from_place, api::Place const& to_place, output const& out, std::optional const start_time, std::optional const end_time, double const max_matching_distance, osr_parameters const& osr_params, street_routing_cache_t& cache, osr::bitvec& blocked_mem, unsigned const api_version, bool const detailed_leg, std::chrono::seconds const max) { utl::verify(start_time.has_value() || end_time.has_value(), "either start_time or end_time must be set"); auto const bound_time = start_time.or_else([&]() { return end_time; }).value(); auto const from = get_location(from_place); auto const to = get_location(to_place); auto const s = e ? get_states_at(w, l, *e, bound_time, from.pos_) : std::optional{std::pair{}}; auto const cache_key = street_routing_cache_key_t{ from, to, out.get_cache_key(), out.is_time_dependent() ? bound_time : n::unixtime_t{n::i32_minutes{0}}}; auto const path = utl::get_or_create(cache, cache_key, [&]() { auto const& [e_nodes, e_states] = *s; auto const profile = out.get_profile(); return osr::route( to_profile_parameters(profile, osr_params), w, l, profile, from, to, static_cast(max.count()), osr::direction::kForward, max_matching_distance, s ? &set_blocked(e_nodes, e_states, blocked_mem) : nullptr, out.get_sharing_data(), elevations, osr::routing_algorithm::kAStarBi); }); if (!path.has_value()) { if (!start_time.has_value() || !end_time.has_value()) { return {}; } return dummy_itinerary(from_place, to_place, out.get_mode(), *start_time, *end_time); } auto const deduced_start_time = start_time ? *start_time : *end_time - std::chrono::seconds{path->cost_}; auto itinerary = api::Itinerary{ .duration_ = start_time && end_time ? std::chrono::duration_cast( *end_time - *start_time) .count() : path->cost_, .startTime_ = deduced_start_time, .endTime_ = end_time ? *end_time : *start_time + std::chrono::seconds{path->cost_}, .transfers_ = 0}; auto t = std::chrono::time_point_cast(deduced_start_time); auto pred_place = from_place; auto pred_end_time = t; utl::equal_ranges_linear( path->segments_, [](osr::path::segment const& a, osr::path::segment const& b) { return a.mode_ == b.mode_; }, [&](std::vector::const_iterator const& lb, std::vector::const_iterator const& ub) { auto const range = std::span{lb, ub}; auto const is_last_leg = ub == end(path->segments_); auto const from_node = range.front().from_; auto const to_node = range.back().to_; auto concat = geo::polyline{}; auto dist = 0.0; for (auto const& p : range) { utl::concat(concat, p.polyline_); if (p.cost_ != osr::kInfeasible) { t += std::chrono::seconds{p.cost_}; dist += p.dist_; } } auto& leg = itinerary.legs_.emplace_back(api::Leg{ .mode_ = out.get_mode() == api::ModeEnum::ODM ? api::ModeEnum::ODM : (out.get_mode() == api::ModeEnum::RIDE_SHARING ? api::ModeEnum::RIDE_SHARING : to_mode(lb->mode_)), .from_ = pred_place, .to_ = is_last_leg ? to_place : out.get_place(lang, to_node, pred_place.tz_), .duration_ = std::chrono::duration_cast( t - pred_end_time) .count(), .startTime_ = pred_end_time, .endTime_ = is_last_leg && end_time ? *end_time : t, .distance_ = dist, .legGeometry_ = detailed_leg ? (api_version == 1 ? to_polyline<7>(concat) : to_polyline<6>(concat)) : empty_polyline()}); leg.from_.departure_ = leg.from_.scheduledDeparture_ = leg.scheduledStartTime_ = leg.startTime_; leg.to_.arrival_ = leg.to_.scheduledArrival_ = leg.scheduledEndTime_ = leg.endTime_; leg.from_.pickupType_ = std::nullopt; leg.from_.dropoffType_ = std::nullopt; leg.to_.pickupType_ = std::nullopt; leg.to_.dropoffType_ = std::nullopt; if (detailed_leg) { leg.steps_ = get_step_instructions(w, elevations, from, to, range, api_version); } out.annotate_leg(lang, from_node, to_node, leg); pred_place = leg.to_; pred_end_time = t; }); if (end_time && !itinerary.legs_.empty()) { auto& last = itinerary.legs_.back(); last.to_.arrival_ = last.to_.scheduledArrival_ = last.endTime_ = last.scheduledEndTime_ = *end_time; for (auto& leg : itinerary.legs_) { leg.duration_ = (leg.endTime_.time_ - leg.startTime_.time_).count(); } } return itinerary; } } // namespace motis ================================================ FILE: src/parse_location.cc ================================================ #include "motis/parse_location.h" #include #include "boost/phoenix/core/reference.hpp" #include "boost/spirit/include/qi.hpp" #include "date/date.h" #include "utl/parser/arg_parser.h" namespace n = nigiri; namespace motis { std::optional parse_location(std::string_view s, char const separator) { using boost::phoenix::ref; using boost::spirit::ascii::space; using boost::spirit::qi::double_; using boost::spirit::qi::phrase_parse; auto first = begin(s); auto last = end(s); auto pos = geo::latlng{}; auto level = osr::kNoLevel; auto const lat = [&](double& x) { pos.lat_ = x; }; auto const lng = [&](double& x) { pos.lng_ = x; }; auto const lvl = [&](double& x) { level = osr::level_t{static_cast(x)}; }; auto const has_matched = phrase_parse(first, last, ((double_[lat] >> separator >> double_[lng] >> separator >> double_[lvl]) | double_[lat] >> separator >> double_[lng]), space); if (!has_matched || first != last) { return std::nullopt; } return osr::location{pos, level}; } date::sys_days parse_iso_date(std::string_view s) { auto d = date::sys_days{}; (std::stringstream{} << s) >> date::parse("%F", d); return d; } std::pair parse_cursor(std::string_view s) { auto const split_pos = s.find("|"); utl::verify(split_pos != std::string_view::npos && split_pos != s.size() - 1U, "invalid page cursor {}, separator '|' not found", s); auto const time_str = s.substr(split_pos + 1U); utl::verify( utl::all_of(time_str, [&](auto&& c) { return std::isdigit(c) != 0U; }), "invalid page cursor \"{}\", timestamp not a number", s); auto const t = n::unixtime_t{std::chrono::duration_cast( std::chrono::seconds{utl::parse(time_str)})}; auto const direction = s.substr(0, split_pos); switch (cista::hash(direction)) { case cista::hash("EARLIER"): return {n::direction::kBackward, t}; case cista::hash("LATER"): return {n::direction::kForward, t}; default: throw utl::fail("invalid cursor: \"{}\"", s); } } n::routing::query cursor_to_query(std::string_view s) { auto const [dir, t] = parse_cursor(s); switch (dir) { case n::direction::kBackward: return n::routing::query{ .start_time_ = n::routing::start_time_t{n::interval{t - n::duration_t{120}, t}}, .extend_interval_earlier_ = true, .extend_interval_later_ = false}; case n::direction::kForward: return n::routing::query{ .start_time_ = n::routing::start_time_t{n::interval{t, t + n::duration_t{120}}}, .extend_interval_earlier_ = false, .extend_interval_later_ = true}; } std::unreachable(); } } // namespace motis ================================================ FILE: src/place.cc ================================================ #include "motis/place.h" #include #include "utl/verify.h" #include "osr/location.h" #include "osr/platforms.h" #include "nigiri/rt/frun.h" #include "nigiri/special_stations.h" #include "nigiri/timetable.h" #include "motis/parse_location.h" #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" namespace n = nigiri; namespace motis { tt_location::tt_location(nigiri::rt::run_stop const& stop) : l_{stop.get_location_idx()}, scheduled_{stop.get_scheduled_location_idx()} {} tt_location::tt_location(nigiri::location_idx_t const l, nigiri::location_idx_t const scheduled) : l_{l}, scheduled_{scheduled == n::location_idx_t::invalid() ? l : scheduled} {} api::Place to_place(osr::location const l, std::string_view name, std::optional const& tz) { return { .name_ = std::string{name}, .lat_ = l.pos_.lat_, .lon_ = l.pos_.lng_, .level_ = l.lvl_.to_float(), .tz_ = tz, .vertexType_ = api::VertexTypeEnum::NORMAL, }; } osr::level_t get_lvl(osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, n::location_idx_t const l) { return w && pl && matches ? pl->get_level(*w, (*matches).at(l)) : osr::kNoLevel; } double get_level(osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, n::location_idx_t const l) { return get_lvl(w, pl, matches, l).to_float(); } osr::location get_location(api::Place const& p) { return {{p.lat_, p.lon_}, osr::level_t{static_cast(p.level_)}}; } osr::location get_location(n::timetable const* tt, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, place_t const loc, place_t const start, place_t const dest) { return std::visit( utl::overloaded{ [&](osr::location const& l) { return l; }, [&](tt_location const l) { auto l_idx = l.l_; if (l_idx == static_cast( n::special_station::kStart)) { if (std::holds_alternative(start)) { return std::get(start); } l_idx = std::get(start).l_; } else if (l_idx == static_cast( n::special_station::kEnd)) { if (std::holds_alternative(dest)) { return std::get(dest); } l_idx = std::get(dest).l_; } utl::verify(tt != nullptr, "resolving stop coordinates: timetable not set"); return osr::location{tt->locations_.coordinates_.at(l_idx), get_lvl(w, pl, matches, l_idx)}; }}, loc); } api::Place to_place(n::timetable const* tt, tag_lookup const* tags, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const* ae, tz_map_t const* tz_map, n::lang_t const& lang, place_t const l, place_t const start, place_t const dest, std::string_view name, std::optional const& fallback_tz) { return std::visit( utl::overloaded{ [&](osr::location const& l) { return to_place(l, name, fallback_tz); }, [&](tt_location const tt_l) -> api::Place { utl::verify(tt && tags, "resolving stops requires timetable"); auto l = tt_l.l_; if (l == n::get_special_station(n::special_station::kStart)) { if (std::holds_alternative(start)) { return to_place(std::get(start), "START", fallback_tz); } l = std::get(start).l_; } else if (l == n::get_special_station(n::special_station::kEnd)) { if (std::holds_alternative(dest)) { return to_place(std::get(dest), "END", fallback_tz); } l = std::get(dest).l_; } auto const get_track = [&](n::location_idx_t const x) { auto const p = tt->translate(lang, tt->locations_.platform_codes_.at(x)); return p.empty() ? std::nullopt : std::optional{std::string{p}}; }; // check if description is available, if not, return nullopt auto const get_description = [&](n::location_idx_t const x) { auto const p = tt->translate(lang, tt->locations_.descriptions_.at(x)); return p.empty() ? std::nullopt : std::optional{std::string{p}}; }; auto const pos = tt->locations_.coordinates_[l]; auto const p = tt->locations_.get_root_idx(l); auto const timezone = get_tz(*tt, ae, tz_map, p); return { .name_ = std::string{tt->translate( lang, tt->locations_.names_.at(p))}, .stopId_ = tags->id(*tt, l), .parentId_ = p == n::location_idx_t::invalid() || p == l ? std::nullopt : std::optional{tags->id(*tt, p)}, .importance_ = ae == nullptr ? std::nullopt : std::optional{ae->place_importance_.at( ae->location_place_.at(l))}, .lat_ = pos.lat_, .lon_ = pos.lng_, .level_ = get_level(w, pl, matches, l), .tz_ = timezone == nullptr ? fallback_tz : std::optional{timezone->name()}, .scheduledTrack_ = get_track(tt_l.scheduled_), .track_ = get_track(tt_l.l_), .description_ = get_description(tt_l.scheduled_), .vertexType_ = api::VertexTypeEnum::TRANSIT, .modes_ = ae != nullptr ? std::optional>{to_modes( ae->place_clasz_.at(ae->location_place_.at(p)), 5)} : std::nullopt}; }}, l); } api::Place to_place(n::timetable const* tt, tag_lookup const* tags, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const* ae, tz_map_t const* tz_map, n::lang_t const& lang, n::rt::run_stop const& s, place_t const start, place_t const dest) { auto const run_cancelled = s.fr_->is_cancelled(); auto const fallback_tz = s.get_tz_name( s.stop_idx_ == 0 ? n::event_type::kDep : n::event_type::kArr); auto p = to_place(tt, tags, w, pl, matches, ae, tz_map, lang, tt_location{s}, start, dest, "", fallback_tz); p.pickupType_ = !run_cancelled && s.in_allowed() ? api::PickupDropoffTypeEnum::NORMAL : api::PickupDropoffTypeEnum::NOT_ALLOWED; p.dropoffType_ = !run_cancelled && s.out_allowed() ? api::PickupDropoffTypeEnum::NORMAL : api::PickupDropoffTypeEnum::NOT_ALLOWED; p.cancelled_ = run_cancelled || (!s.in_allowed() && !s.out_allowed() && (s.get_scheduled_stop().in_allowed() || s.get_scheduled_stop().out_allowed())); return p; } place_t get_place(n::timetable const* tt, tag_lookup const* tags, std::string_view input) { if (auto const location = parse_location(input); location.has_value()) { return *location; } utl::verify(tt != nullptr && tags != nullptr, R"(could not parse location (no timetable loaded): "{}")", input); return tt_location{tags->get_location(*tt, input)}; } } // namespace motis ================================================ FILE: src/polyline.cc ================================================ #include "motis/polyline.h" #include "geo/polyline_format.h" namespace motis { template api::EncodedPolyline to_polyline(geo::polyline const& polyline) { return {geo::encode_polyline(polyline), Precision, static_cast(polyline.size())}; } api::EncodedPolyline empty_polyline() { return api::EncodedPolyline{.points_ = "", .precision_ = 6, .length_ = 0}; } template api::EncodedPolyline to_polyline<5>(geo::polyline const&); template api::EncodedPolyline to_polyline<6>(geo::polyline const&); template api::EncodedPolyline to_polyline<7>(geo::polyline const&); } // namespace motis ================================================ FILE: src/railviz.cc ================================================ #include "motis/railviz.h" #include #include #include #include #include #include #include #include "cista/containers/rtree.h" #include "cista/reflection/comparable.h" #include "net/bad_request_exception.h" #include "net/not_found_exception.h" #include "net/too_many_exception.h" #include "utl/enumerate.h" #include "utl/get_or_create.h" #include "utl/helpers/algorithm.h" #include "utl/pairwise.h" #include "utl/to_vec.h" #include "utl/verify.h" #include "geo/box.h" #include "geo/detail/register_box.h" #include "geo/latlng.h" #include "geo/polyline_format.h" #include "nigiri/common/interval.h" #include "nigiri/common/linear_lower_bound.h" #include "nigiri/routing/journey.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/rt/run.h" #include "nigiri/shapes_storage.h" #include "nigiri/timetable.h" #include "nigiri/types.h" #include "motis/data.h" #include "motis/journey_to_response.h" #include "motis/parse_location.h" #include "motis/place.h" #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" #include "motis/timetable/time_conv.h" namespace n = nigiri; constexpr auto const kTripsLimit = 12'000U; constexpr auto const kRoutesLimit = 20'000U; constexpr auto const kRoutesPolylinesLimit = 20'000U; using static_rtree = cista::raw::rtree; using rt_rtree = cista::raw::rtree; using int_clasz = std::underlying_type_t; namespace motis { struct stop_pair { n::rt::run r_; n::stop_idx_t from_{}, to_{}; }; int min_zoom_level(n::clasz const clasz, float const distance) { switch (clasz) { // long distance case n::clasz::kAir: case n::clasz::kCoach: if (distance < 50'000.F) { return 8; // typically long distance, maybe also quite short } [[fallthrough]]; case n::clasz::kHighSpeed: case n::clasz::kLongDistance: case n::clasz::kNight: return 4; case n::clasz::kRideSharing: case n::clasz::kRegional: return 7; // regional distance case n::clasz::kSuburban: return 8; // metro distance case n::clasz::kSubway: return 9; // short distance case n::clasz::kTram: case n::clasz::kBus: return distance > 10'000.F ? 9 : 10; // ship can be anything case n::clasz::kShip: if (distance > 100'000.F) { return 5; } else if (distance > 10'000.F) { return 8; } else { return 10; } case n::clasz::kODM: case n::clasz::kFunicular: case n::clasz::kAerialLift: case n::clasz::kOther: return 11; default: throw utl::fail("unknown n::clasz {}", static_cast(clasz)); } } bool should_display(n::clasz const clasz, int const zoom_level, float const distance) { return zoom_level >= min_zoom_level(clasz, distance); } struct route_geo_index { route_geo_index() = default; route_geo_index(n::timetable const& tt, n::shapes_storage const* shapes_data, n::clasz const clasz, n::vector_map& distances) { // Fallback, if no route bounding box can be loaded auto const get_box = [&](n::route_idx_t const route_idx) { auto bounding_box = geo::box{}; for (auto const l : tt.route_location_seq_[route_idx]) { bounding_box.extend( tt.locations_.coordinates_.at(n::stop{l}.location_idx())); } return bounding_box; }; for (auto const [i, claszes] : utl::enumerate(tt.route_section_clasz_)) { auto const r = n::route_idx_t{i}; if (claszes.at(0) != clasz) { continue; } auto const bounding_box = (shapes_data == nullptr) ? get_box(r) : shapes_data->get_bounding_box(r); rtree_.insert(bounding_box.min_.lnglat_float(), bounding_box.max_.lnglat_float(), r); distances[r] = static_cast( geo::distance(bounding_box.max_, bounding_box.min_)); } } std::vector get_routes(geo::box const& b) const { auto routes = std::vector{}; rtree_.search(b.min_.lnglat_float(), b.max_.lnglat_float(), [&](auto, auto, n::route_idx_t const r) { routes.push_back(r); return true; }); return routes; } static_rtree rtree_{}; }; struct rt_transport_geo_index { rt_transport_geo_index() = default; rt_transport_geo_index( n::timetable const& tt, n::rt_timetable const& rtt, n::clasz const clasz, n::vector_map& distances) { for (auto const [i, claszes] : utl::enumerate(rtt.rt_transport_section_clasz_)) { auto const rt_t = n::rt_transport_idx_t{i}; if (claszes.at(0) != clasz) { continue; } auto bounding_box = geo::box{}; for (auto const l : rtt.rt_transport_location_seq_[rt_t]) { bounding_box.extend( tt.locations_.coordinates_.at(n::stop{l}.location_idx())); } rtree_.insert(bounding_box.min_.lnglat_float(), bounding_box.max_.lnglat_float(), rt_t); distances[rt_t] = static_cast( geo::distance(bounding_box.min_, bounding_box.max_)); } } std::vector get_rt_transports( n::rt_timetable const& rtt, geo::box const& b) const { auto rt_transports = std::vector{}; rtree_.search(b.min_.lnglat_float(), b.max_.lnglat_float(), [&](auto, auto, n::rt_transport_idx_t const rt_t) { if (!rtt.rt_transport_is_cancelled_[to_idx(rt_t)]) { rt_transports.emplace_back(rt_t); } return true; }); return rt_transports; } rt_rtree rtree_{}; }; struct railviz_static_index::impl { std::array static_geo_indices_; n::vector_map static_distances_{}; }; railviz_static_index::railviz_static_index(n::timetable const& tt, n::shapes_storage const* shapes_data) : impl_{std::make_unique()} { impl_->static_distances_.resize(tt.route_location_seq_.size()); for (auto c = int_clasz{0U}; c != n::kNumClasses; ++c) { impl_->static_geo_indices_[c] = route_geo_index{tt, shapes_data, n::clasz{c}, impl_->static_distances_}; } } railviz_static_index::~railviz_static_index() = default; struct railviz_rt_index::impl { std::array rt_geo_indices_; n::vector_map rt_distances_{}; }; railviz_rt_index::railviz_rt_index(nigiri::timetable const& tt, nigiri::rt_timetable const& rtt) : impl_{std::make_unique()} { impl_->rt_distances_.resize(rtt.rt_transport_location_seq_.size()); for (auto c = int_clasz{0U}; c != n::kNumClasses; ++c) { impl_->rt_geo_indices_[c] = rt_transport_geo_index{tt, rtt, n::clasz{c}, impl_->rt_distances_}; } } railviz_rt_index::~railviz_rt_index() = default; void add_rt_transports(n::timetable const& tt, n::rt_timetable const& rtt, n::rt_transport_idx_t const rt_t, n::interval const time_interval, geo::box const& area, std::vector& runs) { auto const fr = n::rt::frun::from_rt(tt, &rtt, rt_t); for (auto const [from, to] : utl::pairwise(fr)) { auto const box = geo::make_box({from.pos(), to.pos()}); if (!box.overlaps(area)) { continue; } auto const active = n::interval{from.time(n::event_type::kDep), to.time(n::event_type::kArr) + n::i32_minutes{1}}; if (active.overlaps(time_interval)) { utl::verify(runs.size() < kTripsLimit, "too many trips"); runs.emplace_back( stop_pair{.r_ = fr, // NOLINT(cppcoreguidelines-slicing) .from_ = from.stop_idx_, .to_ = to.stop_idx_}); } } } void add_static_transports(n::timetable const& tt, n::rt_timetable const* rtt, n::route_idx_t const r, n::interval const time_interval, geo::box const& area, n::shapes_storage const* shapes_data, std::vector& runs) { auto const is_active = [&](n::transport const t) -> bool { return (rtt == nullptr ? tt.bitfields_[tt.transport_traffic_days_[t.t_idx_]] : rtt->bitfields_[rtt->transport_traffic_days_[t.t_idx_]]) .test(to_idx(t.day_)); }; auto const seq = tt.route_location_seq_[r]; auto const stop_indices = n::interval{n::stop_idx_t{0U}, static_cast(seq.size())}; auto const [start_day, _] = tt.day_idx_mam(time_interval.from_); auto const [end_day, _1] = tt.day_idx_mam(time_interval.to_); auto const get_box = [&](std::size_t segment) { if (shapes_data != nullptr) { auto const box = shapes_data->get_bounding_box(r, segment); if (box.has_value()) { return *box; } } // Fallback, if no segment bounding box can be loaded return geo::make_box( {tt.locations_.coordinates_[n::stop{seq[segment]}.location_idx()], tt.locations_.coordinates_[n::stop{seq[segment + 1]}.location_idx()]}); }; for (auto const [from, to] : utl::pairwise(stop_indices)) { // TODO dwell times auto const box = get_box(from); if (!box.overlaps(area)) { continue; } auto const arr_times = tt.event_times_at_stop(r, to, n::event_type::kArr); for (auto const [i, t_idx] : utl::enumerate(tt.route_transport_ranges_[r])) { auto const day_offset = static_cast(arr_times[i].days()); for (auto traffic_day = start_day - day_offset; traffic_day <= end_day; ++traffic_day) { auto const t = n::transport{t_idx, traffic_day}; if (time_interval.overlaps({tt.event_time(t, from, n::event_type::kDep), tt.event_time(t, to, n::event_type::kArr) + n::unixtime_t::duration{1}}) && is_active(t)) { utl::verify(runs.size() < kTripsLimit, "too many trips"); runs.emplace_back(stop_pair{ .r_ = n::rt::run{.t_ = t, .stop_range_ = {from, static_cast( to + 1U)}, .rt_ = n::rt_transport_idx_t::invalid()}, .from_ = 0, .to_ = 1}); } } } } } api::trips_response get_trains(tag_lookup const& tags, n::timetable const& tt, n::rt_timetable const* rtt, n::shapes_storage const* shapes, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const* ae, tz_map_t const* tz, railviz_static_index::impl const& static_index, railviz_rt_index::impl const& rt_index, api::trips_params const& query, unsigned const api_version) { // Parse query. auto const zoom_level = static_cast(query.zoom_); auto const min = parse_location(query.min_); auto const max = parse_location(query.max_); utl::verify(min.has_value(), "min not a coordinate: {}", query.min_); utl::verify(max.has_value(), "max not a coordinate: {}", query.max_); auto const start_time = std::chrono::time_point_cast(*query.startTime_); auto const end_time = std::chrono::time_point_cast(*query.endTime_); auto const time_interval = n::interval{start_time, end_time}; auto const area = geo::make_box({min->pos_, max->pos_}); // Collect runs within time+location window. auto runs = std::vector{}; for (auto c = int_clasz{0U}; c != n::kNumClasses; ++c) { auto const cl = n::clasz{c}; if (!should_display(cl, zoom_level, std::numeric_limits::infinity())) { continue; } if (rtt != nullptr) { for (auto const& rt_t : rt_index.rt_geo_indices_[c].get_rt_transports(*rtt, area)) { if (should_display(cl, zoom_level, rt_index.rt_distances_[rt_t])) { add_rt_transports(tt, *rtt, rt_t, time_interval, area, runs); } } } for (auto const& r : static_index.static_geo_indices_[c].get_routes(area)) { if (should_display(cl, zoom_level, static_index.static_distances_[r])) { add_static_transports(tt, rtt, r, time_interval, area, shapes, runs); } } } auto const precision = static_cast(query.precision_); utl::verify( precision >= 0 && precision < 7, "invalid precision for polylines, allowed are [0, 6]"); return geo::with_polyline_encoder(precision, [&](auto enc) mutable { return utl::to_vec(runs, [&](stop_pair const& r) -> api::TripSegment { enc.reset(); auto const fr = n::rt::frun{tt, rtt, r.r_}; auto const from = fr[r.from_]; auto const to = fr[r.to_]; fr.for_each_shape_point(shapes, {r.from_, static_cast(r.to_ + 1U)}, [&](auto&& p) { enc.push_nonzero_diff(p, 2); }); return { .trips_ = {api::TripInfo{ .tripId_ = tags.id(tt, from, n::event_type::kDep), .routeShortName_ = api_version < 4 ? std::optional{std::string{from.display_name( n::event_type::kDep, query.language_)}} : std::nullopt, .displayName_ = api_version >= 4 ? std::optional{std::string{from.display_name( n::event_type::kDep, query.language_)}} : std::nullopt}}, .routeColor_ = to_str(from.get_route_color(n::event_type::kDep).color_), .mode_ = to_mode(from.get_clasz(n::event_type::kDep), api_version), .distance_ = fr.is_rt() ? rt_index.rt_distances_[fr.rt_] : static_index .static_distances_[tt.transport_route_[fr.t_.t_idx_]], .from_ = to_place(&tt, &tags, w, pl, matches, ae, tz, query.language_, tt_location{from}), .to_ = to_place(&tt, &tags, w, pl, matches, ae, tz, query.language_, tt_location{to}), .departure_ = from.time(n::event_type::kDep), .arrival_ = to.time(n::event_type::kArr), .scheduledDeparture_ = from.scheduled_time(n::event_type::kDep), .scheduledArrival_ = to.scheduled_time(n::event_type::kArr), .realTime_ = fr.is_rt(), .polyline_ = std::move(enc.buf_)}; }); }); } struct route_info { CISTA_COMPARABLE() std::string_view id_; std::string_view short_name_; std::string_view long_name_; nigiri::route_color color_; }; template Response build_routes_response( tag_lookup const& tags, n::timetable const& tt, n::shapes_storage const* shapes, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, std::vector const& route_indexes, std::optional const& area, std::optional> const& language) { auto res = Response{}; auto enc = geo::polyline_encoder<6>{}; auto const add_unique = [](auto& values, auto const& value) { if (utl::find(values, value) == end(values)) { values.emplace_back(value); } }; auto stop_indexes = std::vector{}; stop_indexes.resize(tt.locations_.coordinates_.size(), -1); auto const get_stop_index = [&](n::rt::run_stop const& stop) { auto const l = stop.get_location_idx(); auto& stop_index = stop_indexes[to_idx(l)]; if (stop_index == -1) { auto const parent = tt.locations_.get_root_idx(l); auto const root = tt.locations_.get_root_idx(l); auto const pos = tt.locations_.coordinates_.at(l); stop_index = static_cast(res.stops_.size()); res.stops_.emplace_back( api::Place{.name_ = std::string{tt.translate( language, tt.locations_.names_.at(root))}, .stopId_ = tags.id(tt, l), .parentId_ = parent == n::location_idx_t::invalid() ? std::nullopt : std::optional{tags.id(tt, parent)}, .lat_ = pos.lat_, .lon_ = pos.lng_, .level_ = get_lvl(w, pl, matches, l).to_float()}); } return stop_index; }; auto polyline_indexes = hash_map{}; auto const get_polyline_index = [&](std::string points, std::int64_t const length) { auto const new_index = static_cast(res.polylines_.size()); auto const [it, inserted] = polyline_indexes.try_emplace(points, new_index); if (inserted) { utl::verify( res.polylines_.size() < kRoutesPolylinesLimit, "too many polylines"); res.polylines_.emplace_back(api::RoutePolyline{ .polyline_ = api::EncodedPolyline{.points_ = std::move(points), .precision_ = 6, .length_ = length}, .colors_ = {}, .routeIndexes_ = {}}); return new_index; } return it->second; }; for (auto const r : route_indexes) { utl::verify(res.routes_.size() < kRoutesLimit, "too many routes"); auto route_segments = std::vector{}; auto route_polyline_indexes = std::vector{}; auto route_infos = hash_set{}; auto const stops = tt.route_location_seq_[r]; auto shape_added = false; auto const get_box = [&](std::size_t segment) { if (shapes != nullptr) { auto const box = shapes->get_bounding_box(r, segment); if (box.has_value()) { return *box; } } // Fallback, if no segment bounding box can be loaded return geo::make_box( {tt.locations_.coordinates_[n::stop{stops[segment]}.location_idx()], tt.locations_ .coordinates_[n::stop{stops[segment + 1U]}.location_idx()]}); }; auto path_source = api::RoutePathSourceEnum::NONE; auto const cl = tt.route_clasz_[r]; for (auto const transport_idx : tt.route_transport_ranges_[r]) { auto const stop_indices = n::interval{ n::stop_idx_t{0U}, static_cast(stops.size())}; for (auto const [from, to] : utl::pairwise(stop_indices)) { enc.reset(); auto n_points = 0; auto const fr = n::rt::frun{ tt, nullptr, n::rt::run{ .t_ = n::transport{transport_idx, n::day_idx_t{0}}, .stop_range_ = n::interval{from, static_cast(to + 1U)}, .rt_ = n::rt_transport_idx_t::invalid()}}; if (from == stop_indices.from_) { route_infos.emplace(route_info{ .id_ = fr[0].get_route_id(n::event_type::kDep), .short_name_ = fr[0].route_short_name(n::event_type::kDep, language), .long_name_ = fr[0].route_long_name(n::event_type::kDep, language), .color_ = fr[0].get_route_color(n::event_type::kDep)}); } if (shape_added || (area.has_value() && !get_box(from).overlaps(*area))) { continue; } fr.for_each_shape_point( shapes, n::interval{n::stop_idx_t{0U}, n::stop_idx_t{2U}}, [&](auto&& p) { enc.push(p); ++n_points; }); if (fr.is_scheduled()) { auto const trp_idx = fr.trip_idx(); auto const shp_idx = shapes->get_shape_idx(trp_idx); if (shp_idx != n::scoped_shape_idx_t::invalid()) { switch (n::get_shape_source(shp_idx)) { case n::shape_source::kNone: path_source = api::RoutePathSourceEnum::NONE; break; case n::shape_source::kTimetable: path_source = api::RoutePathSourceEnum::TIMETABLE; break; case n::shape_source::kRouted: path_source = api::RoutePathSourceEnum::ROUTED; break; } } } route_segments.emplace_back(api::RouteSegment{ .from_ = get_stop_index(fr[0]), .to_ = get_stop_index(fr[1]), .polyline_ = get_polyline_index(std::move(enc.buf_), n_points), }); add_unique(route_polyline_indexes, route_segments.back().polyline_); } shape_added = true; } auto route_colors = std::vector{}; for (auto const& ri : route_infos) { if (auto const color = to_str(ri.color_.color_); color.has_value()) { add_unique(route_colors, *color); } } auto const route_index = static_cast(res.routes_.size()); for (auto const polyline_index : route_polyline_indexes) { auto& polyline = res.polylines_[static_cast(polyline_index)]; add_unique(polyline.routeIndexes_, route_index); for (auto const& color : route_colors) { add_unique(polyline.colors_, color); } } res.routes_.emplace_back(api::RouteInfo{ .mode_ = to_mode(cl, 5), .transitRoutes_ = utl::to_vec(route_infos, [&](auto const& ri) { return api::TransitRouteInfo{ .id_ = std::string{ri.id_}, .shortName_ = std::string{ri.short_name_}, .longName_ = std::string{ri.long_name_}, .color_ = to_str(ri.color_.color_), .textColor_ = to_str(ri.color_.text_color_)}; }), .numStops_ = static_cast(stops.size()), .routeIdx_ = to_idx(r), .pathSource_ = path_source, .segments_ = std::move(route_segments), }); } return res; } api::routes_response get_routes(tag_lookup const& tags, n::timetable const& tt, n::rt_timetable const*, n::shapes_storage const* shapes, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const*, tz_map_t const*, railviz_static_index::impl const& static_index, railviz_rt_index::impl const&, api::routes_params const& query, unsigned const /*api_version*/) { auto const zoom_level = static_cast(query.zoom_); auto const min = parse_location(query.min_); auto const max = parse_location(query.max_); utl::verify(min.has_value(), "min not a coordinate: {}", query.min_); utl::verify(max.has_value(), "max not a coordinate: {}", query.max_); auto const area = geo::make_box({min->pos_, max->pos_}); auto route_indexes = std::vector{}; auto zoom_filtered = false; static_assert(static_cast(n::clasz::kAir) == 0U); // skip air for (auto c = int_clasz{1U}; c != n::kNumClasses; ++c) { auto const cl = n::clasz{c}; if (!should_display(cl, zoom_level, std::numeric_limits::infinity())) { zoom_filtered = true; continue; } for (auto const& r : static_index.static_geo_indices_[c].get_routes(area)) { if (should_display(cl, zoom_level, static_index.static_distances_[r])) { route_indexes.emplace_back(r); } else { zoom_filtered = true; } } } auto res = build_routes_response( tags, tt, shapes, w, pl, matches, route_indexes, area, query.language_); res.zoomFiltered_ = zoom_filtered; return res; } api::routeDetails_response get_route_details( tag_lookup const& tags, n::timetable const& tt, n::rt_timetable const*, n::shapes_storage const* shapes, osr::ways const* w, osr::platforms const* pl, platform_matches_t const* matches, adr_ext const*, tz_map_t const*, railviz_static_index::impl const&, railviz_rt_index::impl const&, api::routeDetails_params const& query, unsigned const /*api_version*/) { utl::verify( query.routeIdx_ >= 0 && query.routeIdx_ < static_cast(tt.n_routes()), "invalid route index {}", query.routeIdx_); auto const route = n::route_idx_t{static_cast(query.routeIdx_)}; auto res = build_routes_response( tags, tt, shapes, w, pl, matches, {route}, std::nullopt, query.language_); res.zoomFiltered_ = false; return res; } } // namespace motis ================================================ FILE: src/route_shapes.cc ================================================ #include "motis/route_shapes.h" #include #include #include #include #include #include #include #include #include #include "boost/stacktrace.hpp" #include "cista/hashing.h" #include "cista/serialization.h" #include "utl/parallel_for.h" #include "utl/progress_tracker.h" #include "utl/sorted_diff.h" #include "utl/to_vec.h" #include "utl/verify.h" #include "geo/box.h" #include "geo/latlng.h" #include "nigiri/loader/build_lb_graph.h" #include "nigiri/rt/frun.h" #include "nigiri/shapes_storage.h" #include "nigiri/timetable.h" #include "osr/routing/map_matching.h" #include "osr/routing/map_matching_debug.h" #include "osr/routing/parameters.h" #include "osr/routing/profile.h" #include "osr/routing/route.h" #include "osr/types.h" #include "motis/match_platforms.h" #include "motis/types.h" namespace n = nigiri; namespace motis { std::string_view to_string_view(cista::byte_buf const& data) { return {reinterpret_cast(data.data()), data.size()}; } struct shape_cache_payload { shape_cache_key key_; shape_cache_entry entry_; }; using shape_cache_bucket = cista::offset::vector; shape_cache::shape_cache(std::filesystem::path const& path, mdb_size_t const map_size) : last_sync_{std::chrono::steady_clock::now()} { env_.set_mapsize(map_size); env_.set_maxdbs(1); env_.open(path.generic_string().c_str(), lmdb::env_open_flags::NOSUBDIR | lmdb::env_open_flags::NOSYNC); auto txn = lmdb::txn{env_}; txn.dbi_open(lmdb::dbi_flags::CREATE); txn.commit(); } shape_cache::~shape_cache() { sync(); } std::optional shape_cache::get(shape_cache_key const& key) { auto txn = lmdb::txn{env_, lmdb::txn_flags::RDONLY}; auto dbi = txn.dbi_open(); auto const bucket = cista::build_hash(key); auto const value = txn.get(dbi, bucket); if (!value.has_value()) { return std::nullopt; } auto const entries = cista::deserialize(*value); for (auto const& payload : *entries) { if (payload.key_ == key) { return payload.entry_; } } return std::nullopt; } void shape_cache::put(shape_cache_key const& key, shape_cache_entry const& entry) { auto txn = lmdb::txn{env_}; auto dbi = txn.dbi_open(); auto const bucket_key = cista::build_hash(key); auto entries = shape_cache_bucket{}; if (auto const value = txn.get(dbi, bucket_key); value.has_value()) { entries = *cista::deserialize(*value); } if (auto const it = std::find_if(begin(entries), end(entries), [&](shape_cache_payload const& payload) { return payload.key_ == key; }); it != end(entries)) { it->entry_ = entry; } else { entries.emplace_back(shape_cache_payload{.key_ = key, .entry_ = entry}); } auto const serialized = cista::serialize(entries); txn.put(dbi, bucket_key, to_string_view(serialized)); txn.commit(); auto const now = std::chrono::steady_clock::now(); if (now - last_sync_ >= std::chrono::minutes{1}) { sync(); last_sync_ = now; } } void shape_cache::sync() { env_.force_sync(); } std::optional get_profile(n::clasz const clasz) { switch (clasz) { case n::clasz::kBus: case n::clasz::kCoach: case n::clasz::kRideSharing: case n::clasz::kODM: return osr::search_profile::kBus; case n::clasz::kTram: case n::clasz::kHighSpeed: case n::clasz::kLongDistance: case n::clasz::kNight: case n::clasz::kRegional: case n::clasz::kSuburban: case n::clasz::kSubway: case n::clasz::kFunicular: return osr::search_profile::kRailway; case n::clasz::kShip: return osr::search_profile::kFerry; default: return std::nullopt; } } struct route_shape_result { n::vector shape_; n::vector offsets_; geo::box route_bbox_{}; n::vector segment_bboxes_; unsigned segments_routed_{}; unsigned segments_beelined_{}; unsigned dijkstra_early_terminations_{}; unsigned dijkstra_full_runs_{}; }; route_shape_result route_shape( osr::ways const& w, osr::lookup const& lookup, n::timetable const& tt, std::vector const& match_points, osr::search_profile const profile, osr::profile_parameters const& profile_params, n::clasz const clasz, n::route_idx_t const route_idx, std::optional const& debug, bool const debug_enabled) { auto r = route_shape_result{}; r.offsets_.reserve( static_cast(match_points.size())); r.segment_bboxes_.reserve(static_cast( match_points.size() - 1U)); auto debug_fn = std::function)>{nullptr}; if (debug_enabled) { debug_fn = [&debug, route_idx, clasz, &tt]( osr::matched_route const& res, std::function const& get_debug_json) { auto include = debug->all_ || (debug->all_with_beelines_ && res.n_beelined_ > 0U); auto tags = std::set{}; if (debug->route_indices_ && !debug->route_indices_->empty()) { auto const& debug_route_indices = *debug->route_indices_; if (std::ranges::contains(debug_route_indices, to_idx(route_idx))) { include = true; } } if (debug->route_ids_ && !debug->route_ids_->empty()) { auto const& debug_route_ids = *debug->route_ids_; for (auto const transport_idx : tt.route_transport_ranges_[route_idx]) { auto const frun = n::rt::frun{ tt, nullptr, n::rt::run{.t_ = n::transport{transport_idx, n::day_idx_t{0}}, .stop_range_ = n::interval{ n::stop_idx_t{0U}, static_cast( tt.route_location_seq_[route_idx].size())}, .rt_ = n::rt_transport_idx_t::invalid()}}; auto const rsn = frun[0].get_route_id(n::event_type::kDep); if (std::ranges::contains(debug_route_ids, rsn)) { tags.emplace(fmt::format("route_{}", rsn)); include = true; break; } } } if (debug->trips_ && !debug->trips_->empty()) { auto const& debug_trip_ids = *debug->trips_; for (auto const transport_idx : tt.route_transport_ranges_[route_idx]) { auto const frun = n::rt::frun{ tt, nullptr, n::rt::run{.t_ = n::transport{transport_idx}, .stop_range_ = n::interval{ n::stop_idx_t{0U}, static_cast( tt.route_location_seq_[route_idx].size())}, .rt_ = n::rt_transport_idx_t::invalid()}}; frun.for_each_trip([&](n::trip_idx_t const trip_idx, n::interval const) { for (auto const trip_id_idx : tt.trip_ids_[trip_idx]) { auto const trip_id = tt.trip_id_strings_.at(trip_id_idx).view(); if (std::ranges::contains(debug_trip_ids, trip_id)) { tags.emplace(fmt::format("trip_{}", trip_id)); include = true; return; } } }); } } auto const total_duration_ms = res.total_duration_.count(); if (debug->slow_ != 0U && total_duration_ms > debug->slow_) { include = true; tags.emplace("slow"); } if (include) { auto fn = fmt::format("r_{}_{}", to_idx(route_idx), to_str(clasz)); for (auto const& tag : tags) { fn += fmt::format("_{}", tag); } auto out_path = debug->path_ / fmt::format("{}.json.gz", fn); osr::write_map_match_debug(get_debug_json(), out_path); } }; } auto const matched_route = osr::map_match(w, lookup, profile, profile_params, match_points, nullptr, nullptr, debug_fn); r.segments_routed_ = matched_route.n_routed_; r.segments_beelined_ = matched_route.n_beelined_; r.dijkstra_early_terminations_ = matched_route.n_dijkstra_early_terminations_; r.dijkstra_full_runs_ = matched_route.n_dijkstra_full_runs_; utl::verify(matched_route.segment_offsets_.size() == match_points.size(), "[route_shapes] segment offsets ({}) != match points ({})", matched_route.segment_offsets_.size(), match_points.size()); r.segment_bboxes_.resize(static_cast( match_points.size() - 1U)); r.shape_.clear(); r.shape_.reserve(static_cast( matched_route.path_.segments_.size() * 8U)); r.offsets_.clear(); r.offsets_.reserve( static_cast(match_points.size())); r.offsets_.emplace_back(0U); for (auto seg_idx = 0U; seg_idx < match_points.size() - 1U; ++seg_idx) { auto& seg_bbox = r.segment_bboxes_[seg_idx]; if (!r.shape_.empty()) { seg_bbox.extend(r.shape_.back()); } auto const start = matched_route.segment_offsets_[seg_idx]; auto const end = (seg_idx + 1U < match_points.size() - 1U) ? matched_route.segment_offsets_[seg_idx + 1U] : matched_route.path_.segments_.size(); for (auto ps_idx = start; ps_idx < end; ++ps_idx) { auto const& ps = matched_route.path_.segments_[ps_idx]; for (auto const& pt : ps.polyline_) { r.route_bbox_.extend(pt); seg_bbox.extend(pt); } if (!ps.polyline_.empty()) { auto first = ps.polyline_.begin(); if (!r.shape_.empty() && r.shape_.back() == *first) { ++first; } r.shape_.insert(r.shape_.end(), first, ps.polyline_.end()); } } r.offsets_.emplace_back(static_cast(r.shape_.size() - 1U)); } utl::verify(r.offsets_.size() == match_points.size(), "[route_shapes] mismatch: offsets.size()={}, stops.size()={}", r.offsets_.size(), match_points.size()); return r; } boost::json::object route_shape_debug(osr::ways const& w, osr::lookup const& lookup, n::timetable const& tt, n::route_idx_t const route_idx) { utl::verify(route_idx < tt.n_routes(), "invalid route index {}", route_idx); auto const clasz = tt.route_clasz_[route_idx]; auto const profile = get_profile(clasz); utl::verify(profile.has_value(), "route {} has unsupported class {}", route_idx, to_str(clasz)); auto const match_points = utl::to_vec(tt.route_location_seq_[route_idx], [&](auto const stop_idx) { auto const loc_idx = n::stop{stop_idx}.location_idx(); auto const pos = tt.locations_.coordinates_[loc_idx]; return osr::location{pos, osr::kNoLevel}; }); auto debug_json = boost::json::object{}; auto const profile_params = osr::get_parameters(*profile); static_cast(osr::map_match( w, lookup, *profile, profile_params, match_points, nullptr, nullptr, [&](osr::matched_route const&, std::function const& get_debug_json) { debug_json = get_debug_json(); })); return debug_json; } void route_shapes(osr::ways const& w, osr::lookup const& lookup, n::timetable const& tt, n::shapes_storage& shapes, config::timetable::route_shapes const& conf, std::array const& clasz_enabled, shape_cache* cache) { fmt::println(std::clog, "computing shapes"); auto const progress_tracker = utl::get_active_progress_tracker(); progress_tracker->status("Computing shapes") .out_bounds(0.F, 100.F) .in_high(tt.n_routes()); auto routes_matched = 0ULL; auto segments_routed = 0ULL; auto segments_beelined = 0ULL; auto dijkstra_early_terminations = 0ULL; auto dijkstra_full_runs = 0ULL; auto routes_with_existing_shapes = 0ULL; auto cache_hits = 0ULL; auto const& debug = conf.debug_; auto const debug_enabled = debug && !debug->path_.empty() && (debug->all_ || debug->all_with_beelines_ || (debug->trips_ && !debug->trips_->empty()) || (debug->route_ids_ && !debug->route_ids_->empty()) || (debug->route_indices_ && !debug->route_indices_->empty()) || debug->slow_ != 0U); if (debug_enabled) { std::filesystem::create_directories(debug->path_); } std::clog << "\n** route_shapes [start] **\n" << " routes=" << tt.n_routes() << "\n trips=" << tt.n_trips() << "\n shapes.trip_offset_indices_=" << shapes.trip_offset_indices_.size() << "\n shapes.route_bboxes_=" << shapes.route_bboxes_.size() << "\n shapes.route_segment_bboxes_=" << shapes.route_segment_bboxes_.size() << "\n shapes.data=" << shapes.data_.size() << "\n shapes.routed_data=" << shapes.routed_data_.size() << "\n shapes.offsets=" << shapes.offsets_.size() << "\n shapes.trip_offset_indices_=" << shapes.trip_offset_indices_.size() << "\n\n"; shapes.trip_offset_indices_.resize(tt.n_trips()); shapes.route_bboxes_.resize(tt.n_routes()); shapes.route_segment_bboxes_.resize(tt.n_routes()); auto shapes_mutex = std::mutex{}; auto const store_shape = [&](n::route_idx_t const r, n::scoped_shape_idx_t const shape_idx, auto const& offsets, geo::box const& route_bbox, auto const& segment_bboxes, std::vector const& match_points, n::interval const& transports) { shapes.route_bboxes_[r] = route_bbox; auto rsb = shapes.route_segment_bboxes_[r]; if (!rsb.empty()) { if (rsb.size() != segment_bboxes.size()) { fmt::println(std::clog, "[route_shapes] route {}: segment bbox size " "mismatch: storage={}, computed={}", r, rsb.size(), segment_bboxes.size()); } else { for (auto i = 0U; i < segment_bboxes.size(); ++i) { rsb[i] = segment_bboxes[i]; } } } auto range_to_offsets = hash_map, n::shape_offset_idx_t>{}; for (auto const transport_idx : transports) { auto const frun = n::rt::frun{ tt, nullptr, n::rt::run{.t_ = n::transport{transport_idx, n::day_idx_t{0}}, .stop_range_ = n::interval{n::stop_idx_t{0U}, static_cast( match_points.size())}, .rt_ = n::rt_transport_idx_t::invalid()}}; frun.for_each_trip([&](n::trip_idx_t const trip_idx, n::interval const range) { auto const key = std::pair{range.from_, range.to_}; auto it = range_to_offsets.find(key); if (it == end(range_to_offsets)) { auto trip_offsets = std::vector{}; trip_offsets.reserve(static_cast(range.size())); for (auto const i : range) { trip_offsets.push_back(offsets.at(i)); } auto const offsets_idx = shapes.add_offsets(trip_offsets); it = range_to_offsets.emplace(key, offsets_idx).first; } shapes.trip_offset_indices_[trip_idx] = {shape_idx, it->second}; }); } }; auto const process_route = [&](std::size_t const route_idx) { auto const r = n::route_idx_t{static_cast(route_idx)}; auto const clasz = tt.route_clasz_[r]; auto profile = get_profile(clasz); if (!profile || !clasz_enabled[static_cast(clasz)]) { progress_tracker->increment(); return; } auto const profile_params = osr::get_parameters(*profile); auto const stops = tt.route_location_seq_[r]; if (stops.size() < 2U || (conf.max_stops_ != 0U && stops.size() > conf.max_stops_)) { auto l = std::scoped_lock{shapes_mutex}; std::clog << "skipping route " << r << ", " << stops.size() << " stops\n"; progress_tracker->increment(); return; } auto const transports = tt.route_transport_ranges_[r]; if (conf.mode_ == config::timetable::route_shapes::mode::missing) { auto existing_shapes = true; for (auto const transport_idx : transports) { auto const frun = n::rt::frun{ tt, nullptr, n::rt::run{.t_ = n::transport{transport_idx, n::day_idx_t{0}}, .stop_range_ = n::interval{n::stop_idx_t{0U}, static_cast( stops.size())}, .rt_ = n::rt_transport_idx_t::invalid()}}; frun.for_each_trip( [&](n::trip_idx_t const trip_idx, n::interval) { auto const shape_idx = shapes.get_shape_idx(trip_idx); if (shape_idx == n::scoped_shape_idx_t::invalid()) { existing_shapes = false; } }); if (existing_shapes) { ++routes_with_existing_shapes; progress_tracker->increment(); return; } } } auto const match_points = utl::to_vec(stops, [&](auto const stop_idx) { auto const loc_idx = n::stop{stop_idx}.location_idx(); auto const pos = tt.locations_.coordinates_[loc_idx]; return osr::location{pos, osr::level_t{osr::kNoLevel}}; }); try { auto cache_key = cache != nullptr ? std::optional{shape_cache_key{ *profile, cista::offset::to_vec( match_points, [](auto const& mp) { return mp.pos_; })}} : std::nullopt; if (cache != nullptr) { if (auto const ce = cache->get(*cache_key); ce.has_value()) { ++cache_hits; auto const local_shape_idx = n::get_local_shape_idx(ce->shape_idx_); utl::verify(ce->shape_idx_ != n::scoped_shape_idx_t::invalid() && n::get_shape_source(ce->shape_idx_) == n::shape_source::kRouted, "[route_shapes] invalid cached shape index: {}", ce->shape_idx_); utl::verify( local_shape_idx != n::shape_idx_t::invalid() && static_cast(to_idx(local_shape_idx)) < shapes.routed_data_.size(), "[route_shapes] cache routed shape idx out of bounds: {} >= {}", local_shape_idx, shapes.routed_data_.size()); auto const l = std::scoped_lock{shapes_mutex}; store_shape(r, ce->shape_idx_, ce->offsets_, ce->route_bbox_, ce->segment_bboxes_, match_points, transports); progress_tracker->increment(); return; } } auto rsr = route_shape(w, lookup, tt, match_points, *profile, profile_params, clasz, r, debug, debug_enabled); ++routes_matched; segments_routed += rsr.segments_routed_; segments_beelined += rsr.segments_beelined_; dijkstra_early_terminations += rsr.dijkstra_early_terminations_; dijkstra_full_runs += rsr.dijkstra_full_runs_; auto const l = std::scoped_lock{shapes_mutex}; auto const local_shape_idx = static_cast(shapes.routed_data_.size()); auto const shape_idx = n::to_scoped_shape_idx(local_shape_idx, n::shape_source::kRouted); shapes.routed_data_.emplace_back(rsr.shape_); store_shape(r, shape_idx, rsr.offsets_, rsr.route_bbox_, rsr.segment_bboxes_, match_points, transports); if (cache != nullptr) { cache->put( *cache_key, shape_cache_entry{ .shape_idx_ = shape_idx, .offsets_ = cista::offset::to_vec(rsr.offsets_), .route_bbox_ = rsr.route_bbox_, .segment_bboxes_ = cista::offset::to_vec(rsr.segment_bboxes_)}); } } catch (std::exception const& e) { fmt::println(std::clog, "[route_shapes] route {}: map matching failed: {}", r, e.what()); if (auto const trace = boost::stacktrace::stacktrace::from_current_exception(); trace) { std::clog << trace << std::endl; } } progress_tracker->increment(); }; utl::parallel_for_run( tt.n_routes(), process_route, utl::noop_progress_update{}, utl::parallel_error_strategy::QUIT_EXEC, conf.n_threads_); if (cache != nullptr) { cache->sync(); } std::clog << "\n** route_shapes [end] **\n" << " routes=" << tt.n_routes() << "\n trips=" << tt.n_trips() << "\n shapes.trip_offset_indices_=" << shapes.trip_offset_indices_.size() << "\n shapes.route_bboxes_=" << shapes.route_bboxes_.size() << "\n shapes.route_segment_bboxes_=" << shapes.route_segment_bboxes_.size() << "\n shapes.data=" << shapes.data_.size() << "\n shapes.routed_data=" << shapes.routed_data_.size() << "\n shapes.offsets=" << shapes.offsets_.size() << "\n shapes.trip_offset_indices_=" << shapes.trip_offset_indices_.size() << "\n\n"; fmt::println(std::clog, "{} routes matched, {} segments routed, {} segments beelined, " "{} dijkstra early terminations, {} dijkstra full runs\n{} " "routes with existing shapes skipped\n{} cache hits", routes_matched, segments_routed, segments_beelined, dijkstra_early_terminations, dijkstra_full_runs, routes_with_existing_shapes, cache_hits); } } // namespace motis ================================================ FILE: src/rt/auser.cc ================================================ #include "motis/rt/auser.h" #include "pugixml.hpp" #include "nigiri/common/parse_time.h" #include "nigiri/rt/json_to_xml.h" #include "motis/http_req.h" namespace n = nigiri; namespace motis { auser::auser(nigiri::timetable const& tt, n::source_idx_t const s, n::rt::vdv_aus::updater::xml_format const format) : upd_{tt, s, format} {} std::string auser::fetch_url(std::string_view base_url) { return upd_.get_format() == n::rt::vdv_aus::updater::xml_format::kVdv ? fmt::format("{}/auser/fetch?since={}&body_limit={}", base_url, update_state_, kBodySizeLimit) : std::string{base_url}; } n::rt::vdv_aus::statistics auser::consume_update( std::string const& auser_update, n::rt_timetable& rtt, bool const inplace) { auto vdvaus = pugi::xml_document{}; if (upd_.get_format() == n::rt::vdv_aus::updater::xml_format::kSiriJson) { vdvaus = n::rt::to_xml(auser_update); } else { inplace ? vdvaus.load_buffer_inplace( const_cast( reinterpret_cast(auser_update.data())), auser_update.size()) : vdvaus.load_string(auser_update.c_str()); } auto stats = upd_.update(rtt, vdvaus); try { auto const prev_update = update_state_; update_state_ = upd_.get_format() == n::rt::vdv_aus::updater::xml_format::kVdv ? [&]() { auto const opt1 = vdvaus.child("DatenAbrufenAntwort") .child("AUSNachricht") .attribute("auser_id") .as_llong(0ULL); auto const opt2 = vdvaus.child("AUSNachricht") .attribute("auser_id") .as_llong(0ULL); return opt1 ? opt1 : opt2; }() : n::parse_time_no_tz(vdvaus.child("Siri") .child("ServiceDelivery") .child_value("ResponseTimestamp")) .time_since_epoch() .count(); fmt::println("[auser] {} --> {}", prev_update, update_state_); } catch (...) { } return stats; } } // namespace motis ================================================ FILE: src/rt_update.cc ================================================ #include "motis/rt_update.h" #include #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/asio/experimental/parallel_group.hpp" #include "boost/asio/redirect_error.hpp" #include "boost/asio/steady_timer.hpp" #include "boost/beast/core/buffers_to_string.hpp" #include "utl/read_file.h" #include "utl/timer.h" #include "nigiri/rt/create_rt_timetable.h" #include "nigiri/rt/gtfsrt_update.h" #include "nigiri/rt/rt_timetable.h" #include "motis/config.h" #include "motis/data.h" #include "motis/elevators/update_elevators.h" #include "motis/http_req.h" #include "motis/railviz.h" #include "motis/rt/auser.h" #include "motis/rt/rt_metrics.h" #include "motis/tag_lookup.h" namespace n = nigiri; namespace asio = boost::asio; namespace fs = std::filesystem; using asio::awaitable; namespace motis { asio::awaitable> update_elevators(config const& c, data const& d, n::rt_timetable& new_rtt) { utl::verify(c.has_elevators() && c.get_elevators()->url_ && c.timetable_, "elevator update requires settings for timetable + elevators"); auto const res = co_await http_GET(boost::urls::url{*c.get_elevators()->url_}, c.get_elevators()->headers_.value_or(headers_t{}), std::chrono::seconds{c.get_elevators()->http_timeout_}); co_return update_elevators(c, d, get_http_body(res), new_rtt); } std::string get_dump_path(auto&& ep) { auto const normalize = [](std::string const& x) { auto ret = std::string{}; ret.resize(x.size()); for (auto [to, from] : utl::zip(ret, x)) { auto const c = from; if (('0' <= c && c <= '9') || // ('a' <= c && c <= 'z') || // ('A' <= c && c <= 'Z')) { to = c; } else { to = '_'; } } return ret; }; return fmt::format("dump_rt/{}-{}", ep.tag_, normalize(ep.ep_.url_)); } struct gtfs_rt_endpoint { config::timetable::dataset::rt ep_; n::source_idx_t src_; std::string tag_; gtfsrt_metrics metrics_; }; struct auser_endpoint { config::timetable::dataset::rt ep_; n::source_idx_t src_; std::string tag_; vdvaus_metrics metrics_; }; void run_rt_update(boost::asio::io_context& ioc, config const& c, data& d) { boost::asio::co_spawn( ioc, [&c, &d]() -> awaitable { auto const dump_rt = fs::is_directory("dump_rt"); if (dump_rt) { fmt::println("WARNING: DUMPING TO dump_rt\n"); } auto executor = co_await asio::this_coro::executor; auto timer = asio::steady_timer{executor}; auto ec = boost::system::error_code{}; auto const endpoints = [&]() { auto endpoints = std::vector>{}; auto const metric_families = rt_metric_families{d.metrics_->registry_}; for (auto const& [tag, dataset] : c.timetable_->datasets_) { if (dataset.rt_.has_value()) { auto const src = d.tags_->get_src(tag); for (auto const& ep : *dataset.rt_) { switch (ep.protocol_) { case config::timetable::dataset::rt::protocol::gtfsrt: endpoints.push_back(gtfs_rt_endpoint{ ep, src, tag, gtfsrt_metrics{tag, metric_families}}); break; case config::timetable::dataset::rt::protocol::siri_json: case config::timetable::dataset::rt::protocol::siri: [[fallthrough]]; case config::timetable::dataset::rt::protocol::auser: endpoints.push_back(auser_endpoint{ ep, src, tag, vdvaus_metrics{tag, metric_families}}); break; } } } } return endpoints; }(); while (true) { // Remember when we started, so we can schedule the next update. auto const start = std::chrono::steady_clock::now(); { auto t = utl::scoped_timer{"rt update"}; // Create new real-time timetable. auto const today = std::chrono::time_point_cast( std::chrono::system_clock::now()); auto rtt = std::make_unique( c.timetable_->incremental_rt_update_ ? n::rt_timetable{*d.rt_->rtt_} : n::rt::create_rt_timetable(*d.tt_, today)); // Schedule updates for each real-time endpoint. auto const timeout = std::chrono::seconds{c.timetable_->http_timeout_}; using stats_t = std::variant; if (c.timetable_->canned_rt_) { fmt::println("WARNING: READING CANNED RT"); auto const stats = utl::to_vec(endpoints, [&](auto&& ep) -> stats_t { try { return utl::visit( ep, [&](gtfs_rt_endpoint const& g) -> stats_t { auto const path = get_dump_path(g); auto const body = utl::read_file(path.c_str()); if (body.has_value()) { return n::rt::gtfsrt_update_buf( *d.tt_, *rtt, g.src_, g.tag_, *body); } else { return n::rt::statistics{.parser_error_ = true}; } }, [&](auser_endpoint const& a) -> stats_t { auto const path = get_dump_path(a); auto& auser = d.auser_->at(a.ep_.url_); auto const body = utl::read_file(path.c_str()); if (body.has_value()) { return auser.consume_update(*body, *rtt); } else { return n::rt::vdv_aus::statistics{.error_ = true}; } }); } catch (std::exception const& e) { std::cout << "EXCEPTION: " << e.what() << "\n"; return n::rt::statistics{.parser_error_ = true}; } }); for (auto const [s, ep] : utl::zip(stats, endpoints)) { utl::visit( ep, [&](gtfs_rt_endpoint const& g) { n::log(n::log_lvl::info, "motis.rt", "GTFS-RT update stats for tag={}, url={}: {}", g.tag_, g.ep_.url_, fmt::streamed(std::get(s))); }, [&](auser_endpoint const& a) { n::log(n::log_lvl::info, "motis.rt", "VDV AUS update stats for tag={}, url={}:\n{}", a.tag_, a.ep_.url_, fmt::streamed( std::get(s))); }); } } else if (!endpoints.empty()) { auto awaitables = utl::to_vec( endpoints, [&](std::variant const& x) { return boost::asio::co_spawn( executor, [&]() -> awaitable< std::variant> { auto ret = std::variant{}; co_await std::visit( utl::overloaded{ [&](gtfs_rt_endpoint const& g) -> awaitable { g.metrics_.updates_requested_.Increment(); try { auto const res = co_await http_GET( boost::urls::url{g.ep_.url_}, g.ep_.headers_.value_or(headers_t{}), timeout); auto const body = get_http_body(res); if (dump_rt) { std::ofstream{get_dump_path(g)}.write( body.c_str(), static_cast(body.size())); } ret = n::rt::gtfsrt_update_buf( *d.tt_, *rtt, g.src_, g.tag_, body); } catch (std::exception const& e) { g.metrics_.updates_error_.Increment(); n::log(n::log_lvl::error, "motis.rt", "RT FETCH ERROR: tag={}, error={}", g.tag_, e.what()); ret = n::rt::statistics{ .parser_error_ = true, .no_header_ = true}; } }, [&](auser_endpoint const& a) -> awaitable { a.metrics_.updates_requested_.Increment(); auto& auser = d.auser_->at(a.ep_.url_); try { auto const fetch_url = boost::urls::url{ auser.fetch_url(a.ep_.url_)}; fmt::println("[auser] fetch url: {}", fetch_url.c_str()); auto const res = co_await http_GET( fetch_url, a.ep_.headers_.value_or(headers_t{}), timeout); auto body = get_http_body(res); if (dump_rt) { std::ofstream{get_dump_path(a)}.write( body.c_str(), static_cast(body.size())); } ret = auser.consume_update(body, *rtt, true); } catch (std::exception const& e) { a.metrics_.updates_error_.Increment(); n::log(n::log_lvl::error, "motis.rt", "VDV AUS FETCH ERROR: tag={}, " "url={}, error={}", a.tag_, a.ep_.url_, e.what()); ret = nigiri::rt::vdv_aus::statistics{ .error_ = true}; } }}, x); co_return ret; }, asio::deferred); }); // Wait for all updates to finish auto [_, exceptions, stats] = co_await asio::experimental::make_parallel_group(awaitables) .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable); // Print statistics. for (auto const [ep, ex, s] : utl::zip(endpoints, exceptions, stats)) { std::visit( utl::overloaded{ [&](gtfs_rt_endpoint const& g) { try { if (ex) { std::rethrow_exception(ex); } g.metrics_.updates_successful_.Increment(); g.metrics_.last_update_timestamp_ .SetToCurrentTime(); g.metrics_.update(std::get(s)); n::log( n::log_lvl::info, "motis.rt", "GTFS-RT update stats for tag={}, url={}: {}", g.tag_, g.ep_.url_, fmt::streamed(std::get(s))); } catch (std::exception const& e) { g.metrics_.updates_error_.Increment(); n::log(n::log_lvl::error, "motis.rt", "GTFS-RT update failed: tag={}, url={}, " "error={}", g.tag_, g.ep_.url_, e.what()); } }, [&](auser_endpoint const& a) { try { if (ex) { std::rethrow_exception(ex); } a.metrics_.updates_successful_.Increment(); a.metrics_.last_update_timestamp_ .SetToCurrentTime(); a.metrics_.update( std::get(s)); n::log( n::log_lvl::info, "motis.rt", "VDV AUS update stats for tag={}, url={}:\n{}", a.tag_, a.ep_.url_, fmt::streamed( std::get(s))); } catch (std::exception const& e) { a.metrics_.updates_error_.Increment(); n::log(n::log_lvl::error, "motis.rt", "VDV AUS update failed: tag={}, url={}, " "error={}", a.tag_, a.ep_.url_, e.what()); } }}, ep); } } // Update lbs. rtt->update_lbs(*d.tt_); // Update real-time timetable shared pointer. auto railviz_rt = std::make_unique(*d.tt_, *rtt); auto elevators = c.has_elevators() && c.get_elevators()->url_ ? co_await update_elevators(c, d, *rtt) : std::move(d.rt_->e_); auto new_rt = std::make_shared( std::move(rtt), std::move(elevators), std::move(railviz_rt)); std::atomic_store(&d.rt_, std::move(new_rt)); } // Schedule next update. timer.expires_at( start + std::chrono::seconds{c.timetable_->update_interval_}); co_await timer.async_wait( asio::redirect_error(asio::use_awaitable, ec)); if (ec == asio::error::operation_aborted) { co_return; } } }, boost::asio::detached); } } // namespace motis ================================================ FILE: src/server.cc ================================================ #include #include "boost/asio/io_context.hpp" #include "fmt/format.h" #include "net/lb.h" #include "net/run.h" #include "net/stop_handler.h" #include "net/web_server/web_server.h" #include "utl/enumerate.h" #include "utl/init_from.h" #include "utl/logging.h" #include "utl/parser/arg_parser.h" #include "ctx/ctx.h" #include "motis/config.h" #include "motis/ctx_data.h" #include "motis/ctx_exec.h" #include "motis/data.h" #include "motis/motis_instance.h" namespace fs = std::filesystem; namespace motis { int server(data d, config const& c, std::string_view const motis_version) { auto scheduler = ctx::scheduler{}; auto m = motis_instance{ctx_exec{scheduler.runner_.ios(), scheduler}, d, c, motis_version}; auto lbs = std::vector{}; if (c.server_.value_or(config::server{}).lbs_) { lbs = utl::to_vec(*c.server_.value_or(config::server{}).lbs_, [&](std::string const& url) { return net::lb{scheduler.runner_.ios(), url, m.qr_}; }); } auto s = net::web_server{scheduler.runner_.ios()}; s.set_timeout(std::chrono::minutes{5}); s.on_http_request(m.qr_); auto ec = boost::system::error_code{}; auto const server_config = c.server_.value_or(config::server{}); s.init(server_config.host_, server_config.port_, ec); if (ec) { std::cerr << "error: " << ec << "\n"; return 1; } auto const stop = net::stop_handler(scheduler.runner_.ios(), [&]() { utl::log_info("motis.server", "shutdown"); for (auto& lb : lbs) { lb.stop(); } s.stop(); m.stop(); scheduler.runner_.stop(); }); utl::log_info( "motis.server", "n_threads={}, listening on {}:{}\nlocal link: http://localhost:{}", c.n_threads(), server_config.host_, server_config.port_, server_config.port_); for (auto& lb : lbs) { lb.run(); } s.run(); m.run(d, c); scheduler.runner_.run(c.n_threads()); m.join(); return 0; } unsigned get_api_version(boost::urls::url_view const& url) { if (url.encoded_path().length() > 7) { return utl::parse( std::string_view{url.encoded_path().substr(6, 2)}); } return 0U; } } // namespace motis ================================================ FILE: src/tag_lookup.cc ================================================ #include "motis/tag_lookup.h" #include #include "fmt/chrono.h" #include "fmt/core.h" #include "cista/io.h" #include "utl/enumerate.h" #include "utl/parser/split.h" #include "utl/verify.h" #include "net/bad_request_exception.h" #include "net/not_found_exception.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/gtfsrt_resolve_run.h" #include "nigiri/timetable.h" namespace n = nigiri; namespace motis { trip_id split_trip_id(std::string_view id) { auto const [date, start_time, tag, trip_id] = utl::split<'_', utl::cstr, utl::cstr, utl::cstr, utl::cstr>(id); auto ret = motis::trip_id{}; utl::verify(date.valid(), "invalid tripId date {}", id); ret.start_date_ = date.view(); utl::verify(start_time.valid(), "invalid tripId start_time {}", id); ret.start_time_ = start_time.view(); utl::verify(tag.valid(), "invalid tripId tag {}", id); ret.tag_ = tag.view(); // allow trip ids starting with underscore auto const trip_id_len_plus_one = static_cast(id.data() + id.size() - tag.str) - tag.length(); utl::verify(trip_id_len_plus_one > 1, "invalid tripId id {}", id); ret.trip_id_ = std::string_view{tag.str + tag.length() + 1, trip_id_len_plus_one - 1}; return ret; } std::pair split_tag_id(std::string_view x) { auto const first_underscore_pos = x.find('_'); return first_underscore_pos != std::string_view::npos ? std::pair{x.substr(0, first_underscore_pos), x.substr(first_underscore_pos + 1U)} : std::pair{std::string_view{}, x}; } void tag_lookup::add(n::source_idx_t const src, std::string_view str) { utl::verify(tag_to_src_.size() == to_idx(src), "invalid tag"); tag_to_src_.emplace(std::string{str}, src); src_to_tag_.emplace_back(str); } n::source_idx_t tag_lookup::get_src(std::string_view tag) const { auto const it = tag_to_src_.find(tag); return it == end(tag_to_src_) ? n::source_idx_t::invalid() : it->second; } std::string_view tag_lookup::get_tag(n::source_idx_t const src) const { return src == n::source_idx_t::invalid() ? "" : src_to_tag_.at(src).view(); } std::string tag_lookup::id(n::timetable const& tt, n::location_idx_t const l) const { auto const src = tt.locations_.src_.at(l); auto const id = tt.locations_.ids_.at(l).view(); return src == n::source_idx_t::invalid() ? std::string{id} : fmt::format("{}_{}", get_tag(src), id); } trip_id tag_lookup::id_fragments( n::timetable const& tt, n::rt::run_stop s, n::event_type const ev_type) const { if (s.fr_->is_scheduled()) { // trip id auto const t = s.get_trip_idx(ev_type); auto const id_idx = tt.trip_ids_[t].front(); auto const id = tt.trip_id_strings_[id_idx].view(); auto const src = tt.trip_id_src_[id_idx]; // start date + start time auto const [day, gtfs_static_dep] = s.get_trip_start(ev_type); auto const start_hours = gtfs_static_dep / 60; auto const start_minutes = gtfs_static_dep % 60; return { fmt::format("{:%Y%m%d}", day), fmt::format("{:02}:{:02}", start_hours.count(), start_minutes.count()), std::string{get_tag(src)}, std::string{id}}; } else { auto const id = s.fr_->id(); auto const time = std::chrono::system_clock::to_time_t( (*s.fr_)[0].time(n::event_type::kDep)); auto const utc = *std::gmtime(&time); auto const id_tag = get_tag(id.src_); auto const id_id = id.id_; return {fmt::format("{:04}{:02}{:02}", utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday), fmt::format("{:02}:{:02}", utc.tm_hour, utc.tm_min), std::string{id_tag}, std::string{id_id}}; } } std::string tag_lookup::id(n::timetable const& tt, n::rt::run_stop s, n::event_type const ev_type) const { auto const t = id_fragments(tt, s, ev_type); return fmt::format("{}_{}_{}_{}", std::move(t.start_date_), std::move(t.start_time_), std::move(t.tag_), std::move(t.trip_id_)); } std::string tag_lookup::route_id(n::rt::run_stop s, n::event_type const ev_type) const { return fmt::format("{}_{}", get_tag(s.fr_->id().src_), s.get_route_id(ev_type)); } std::pair tag_lookup::get_trip( n::timetable const& tt, n::rt_timetable const* rtt, std::string_view id) const { auto const split = split_trip_id(id); auto td = transit_realtime::TripDescriptor{}; td.set_start_date(split.start_date_); td.set_start_time(split.start_time_); td.set_trip_id(split.trip_id_); return n::rt::gtfsrt_resolve_run({}, tt, rtt, get_src(split.tag_), td); } n::location_idx_t tag_lookup::get_location(n::timetable const& tt, std::string_view s) const { if (auto const res = find_location(tt, s); res.has_value()) { return *res; } throw utl::fail( "Could not find timetable location {:?}", s); } std::optional tag_lookup::find_location( n::timetable const& tt, std::string_view s) const { auto const [tag, id] = split_tag_id(s); auto const src = get_src(tag); if (src == n::source_idx_t::invalid()) { return std::nullopt; } auto const it = tt.locations_.location_id_to_idx_.find({id, src}); if (it == end(tt.locations_.location_id_to_idx_)) { return std::nullopt; } return it->second; } void tag_lookup::write(std::filesystem::path const& p) const { return cista::write(p, *this); } cista::wrapped tag_lookup::read(std::filesystem::path const& p) { return cista::read(p); } std::ostream& operator<<(std::ostream& out, tag_lookup const& tags) { auto first = true; for (auto const [src, tag] : utl::enumerate(tags.src_to_tag_)) { if (!first) { out << ", "; } first = false; out << src << "=" << tag.view(); } return out; } } // namespace motis ================================================ FILE: src/timetable/clasz_to_mode.cc ================================================ #include "motis/timetable/clasz_to_mode.h" #include "utl/for_each_bit_set.h" namespace n = nigiri; namespace motis { api::ModeEnum to_mode(n::clasz const c, unsigned const api_version) { switch (c) { case n::clasz::kAir: return api::ModeEnum::AIRPLANE; case n::clasz::kHighSpeed: return api::ModeEnum::HIGHSPEED_RAIL; case n::clasz::kLongDistance: return api::ModeEnum::LONG_DISTANCE; case n::clasz::kCoach: return api::ModeEnum::COACH; case n::clasz::kNight: return api::ModeEnum::NIGHT_RAIL; case n::clasz::kRideSharing: return api::ModeEnum::RIDE_SHARING; case n::clasz::kRegional: return api::ModeEnum::REGIONAL_RAIL; case n::clasz::kSuburban: return api_version < 5 ? api::ModeEnum::METRO : api::ModeEnum::SUBURBAN; case n::clasz::kSubway: return api::ModeEnum::SUBWAY; case n::clasz::kTram: return api::ModeEnum::TRAM; case n::clasz::kBus: return api::ModeEnum::BUS; case n::clasz::kShip: return api::ModeEnum::FERRY; case n::clasz::kODM: return api::ModeEnum::ODM; case n::clasz::kFunicular: return api::ModeEnum::FUNICULAR; case n::clasz::kAerialLift: return api_version < 5 ? api::ModeEnum::AREAL_LIFT : api::ModeEnum::AERIAL_LIFT; case n::clasz::kOther: return api::ModeEnum::OTHER; case n::clasz::kNumClasses:; } std::unreachable(); } std::vector to_modes(nigiri::routing::clasz_mask_t const mask, unsigned api_version) { auto modes = std::vector{}; utl::for_each_set_bit(mask, [&](auto const i) { modes.emplace_back(to_mode(static_cast(i), api_version)); }); return modes; } } // namespace motis ================================================ FILE: src/timetable/modes_to_clasz_mask.cc ================================================ #include "motis/timetable/modes_to_clasz_mask.h" namespace n = nigiri; namespace motis { n::routing::clasz_mask_t to_clasz_mask(std::vector const& mode) { auto mask = n::routing::clasz_mask_t{0U}; auto const allow = [&](n::clasz const c) { mask |= (1U << static_cast>(c)); }; for (auto const& m : mode) { switch (m) { case api::ModeEnum::TRANSIT: mask = n::routing::all_clasz_allowed(); return mask; case api::ModeEnum::TRAM: allow(n::clasz::kTram); break; case api::ModeEnum::SUBWAY: allow(n::clasz::kSubway); break; case api::ModeEnum::FERRY: allow(n::clasz::kShip); break; case api::ModeEnum::AIRPLANE: allow(n::clasz::kAir); break; case api::ModeEnum::BUS: allow(n::clasz::kBus); break; case api::ModeEnum::COACH: allow(n::clasz::kCoach); break; case api::ModeEnum::RAIL: allow(n::clasz::kHighSpeed); allow(n::clasz::kLongDistance); allow(n::clasz::kNight); allow(n::clasz::kRegional); allow(n::clasz::kSuburban); allow(n::clasz::kSubway); break; case api::ModeEnum::HIGHSPEED_RAIL: allow(n::clasz::kHighSpeed); break; case api::ModeEnum::LONG_DISTANCE: allow(n::clasz::kLongDistance); break; case api::ModeEnum::NIGHT_RAIL: allow(n::clasz::kNight); break; case api::ModeEnum::RIDE_SHARING: allow(n::clasz::kRideSharing); break; case api::ModeEnum::REGIONAL_FAST_RAIL: [[fallthrough]]; case api::ModeEnum::REGIONAL_RAIL: allow(n::clasz::kRegional); break; case api::ModeEnum::SUBURBAN: allow(n::clasz::kSuburban); break; case api::ModeEnum::METRO: allow(n::clasz::kSuburban); break; case api::ModeEnum::ODM: allow(n::clasz::kODM); break; case api::ModeEnum::CABLE_CAR: [[fallthrough]]; case api::ModeEnum::FUNICULAR: allow(n::clasz::kFunicular); break; case api::ModeEnum::AERIAL_LIFT: allow(n::clasz::kAerialLift); break; case api::ModeEnum::AREAL_LIFT: allow(n::clasz::kAerialLift); break; case api::ModeEnum::OTHER: allow(n::clasz::kOther); break; case api::ModeEnum::WALK: case api::ModeEnum::BIKE: case api::ModeEnum::RENTAL: case api::ModeEnum::CAR: case api::ModeEnum::FLEX: case api::ModeEnum::DEBUG_BUS_ROUTE: case api::ModeEnum::DEBUG_RAILWAY_ROUTE: case api::ModeEnum::DEBUG_FERRY_ROUTE: case api::ModeEnum::CAR_DROPOFF: [[fallthrough]]; case api::ModeEnum::CAR_PARKING: break; } } return mask; } } // namespace motis ================================================ FILE: src/update_rtt_td_footpaths.cc ================================================ #include "motis/update_rtt_td_footpaths.h" #include #include "utl/equal_ranges_linear.h" #include "utl/parallel_for.h" #include "osr/routing/parameters.h" #include "osr/routing/route.h" #include "motis/constants.h" #include "motis/get_loc.h" #include "motis/get_stops_with_traffic.h" #include "motis/osr/max_distance.h" namespace n = nigiri; using namespace std::chrono_literals; namespace motis { using node_states_t = std::pair>>; node_states_t get_node_states(osr::ways const& w, osr::lookup const& l, elevators const& e, geo::latlng const& pos) { auto e_nodes = utl::to_vec(l.find_elevators(geo::box{pos, kElevatorUpdateRadius})); auto e_state_changes = get_state_changes( utl::to_vec( e_nodes, [&](osr::node_idx_t const n) -> std::vector> { auto const ne = match_elevator(e.elevators_rtree_, e.elevators_, w, n); if (ne == elevator_idx_t::invalid()) { return { {.valid_from_ = n::unixtime_t{n::unixtime_t::duration{0}}, .state_ = true}}; } return e.elevators_[ne].get_state_changes(); })) .to_vec(); return {std::move(e_nodes), std::move(e_state_changes)}; } osr::bitvec& set_blocked( nodes_t const& e_nodes, states_t const& states, osr::bitvec& blocked_mem) { blocked_mem.zero_out(); for (auto const [n, s] : utl::zip(e_nodes, states)) { blocked_mem.set(n, !s); } return blocked_mem; } std::optional> get_states_at( osr::ways const& w, osr::lookup const& l, elevators const& e, n::unixtime_t const t, geo::latlng const& pos) { auto const [e_nodes, e_state_changes] = get_node_states(w, l, e, pos); if (e_nodes.empty()) { return std::pair{nodes_t{}, states_t{}}; } auto const it = std::lower_bound( begin(e_state_changes), end(e_state_changes), t, [&](auto&& a, n::unixtime_t const b) { return a.first < b; }); if (it == begin(e_state_changes)) { return std::nullopt; } return std::pair{e_nodes, std::prev(it)->second}; } std::vector get_td_footpaths( osr::ways const& w, osr::lookup const& l, osr::platforms const& pl, nigiri::timetable const& tt, nigiri::rt_timetable const* rtt, point_rtree const& loc_rtree, elevators const& e, platform_matches_t const& matches, n::location_idx_t const start_l, osr::location const start, osr::direction const dir, osr::search_profile const profile, std::chrono::seconds const max, double const max_matching_distance, osr_parameters const& osr_params, osr::bitvec& blocked_mem) { blocked_mem.resize(w.n_nodes()); auto const [e_nodes, e_state_changes] = get_node_states(w, l, e, start.pos_); auto fps = std::vector{}; for (auto const& [t, states] : e_state_changes) { set_blocked(e_nodes, states, blocked_mem); auto const neighbors = get_stops_with_traffic( tt, rtt, loc_rtree, start, get_max_distance(profile, max), start_l); auto const results = osr::route( to_profile_parameters(profile, osr_params), w, l, profile, start, utl::to_vec(neighbors, [&](auto&& x) { return get_loc(tt, w, pl, matches, x); }), static_cast(max.count()), dir, max_matching_distance, &blocked_mem); for (auto const [to, p] : utl::zip(neighbors, results)) { auto const duration = p.has_value() && (n::duration_t{p->cost_ / 60U} < n::footpath::kMaxDuration) ? n::duration_t{p->cost_ / 60U} : n::footpath::kMaxDuration; fps.push_back(n::td_footpath{ to, t, n::duration_t{std::max(n::duration_t::rep{1}, duration.count())}}); } } utl::sort(fps); utl::equal_ranges_linear( fps, [](auto const& a, auto const& b) { return a.target_ == b.target_; }, [&](std::vector::iterator& lb, std::vector::iterator& ub) { for (auto it = lb; it != ub; ++it) { if (it->duration_ == n::footpath::kMaxDuration && it != lb && (it - 1)->duration_ != n::footpath::kMaxDuration) { // TODO support feasible, but longer paths it->valid_from_ -= (it - 1)->duration_ - n::duration_t{1U}; } } }); return fps; } void update_rtt_td_footpaths( osr::ways const& w, osr::lookup const& l, osr::platforms const& pl, nigiri::timetable const& tt, point_rtree const& loc_rtree, elevators const& e, platform_matches_t const& matches, hash_set> const& tasks, nigiri::rt_timetable const* old_rtt, nigiri::rt_timetable& rtt, std::chrono::seconds const max) { auto in_mutex = std::mutex{}, out_mutex = std::mutex{}; auto out = std::map>{}; auto in = std::map>{}; utl::parallel_for_run_threadlocal>( tasks.size(), [&](osr::bitvec& blocked, std::size_t const task_idx) { auto const [start, dir] = *(begin(tasks) + task_idx); auto fps = get_td_footpaths(w, l, pl, tt, &rtt, loc_rtree, e, matches, start, get_loc(tt, w, pl, matches, start), dir, osr::search_profile::kWheelchair, max, kMaxWheelchairMatchingDistance, osr_parameters{}, blocked); { auto const lock = std::unique_lock{ dir == osr::direction::kForward ? out_mutex : in_mutex}; (dir == osr::direction::kForward ? out : in)[start] = std::move(fps); } }); rtt.td_footpaths_out_[2].clear(); for (auto i = n::location_idx_t{0U}; i != tt.n_locations(); ++i) { auto const it = out.find(i); if (it != end(out)) { rtt.has_td_footpaths_out_[2].set(i, true); rtt.td_footpaths_out_[2].emplace_back(it->second); } else if (old_rtt != nullptr) { rtt.has_td_footpaths_out_[2].set( i, old_rtt->has_td_footpaths_out_[2].test(i)); rtt.td_footpaths_out_[2].emplace_back(old_rtt->td_footpaths_out_[2][i]); } else { rtt.has_td_footpaths_out_[2].set(i, false); rtt.td_footpaths_out_[2].emplace_back( std::initializer_list{}); } } rtt.td_footpaths_in_[2].clear(); for (auto i = n::location_idx_t{0U}; i != tt.n_locations(); ++i) { auto const it = in.find(i); if (it != end(in)) { rtt.has_td_footpaths_in_[2].set(i, true); rtt.td_footpaths_in_[2].emplace_back(it->second); } else if (old_rtt != nullptr) { rtt.has_td_footpaths_in_[2].set(i, old_rtt->has_td_footpaths_in_[2].test(i)); rtt.td_footpaths_in_[2].emplace_back(old_rtt->td_footpaths_in_[2][i]); } else { rtt.has_td_footpaths_in_[2].set(i, false); rtt.td_footpaths_in_[2].emplace_back( std::initializer_list{}); } } } void update_rtt_td_footpaths(osr::ways const& w, osr::lookup const& l, osr::platforms const& pl, nigiri::timetable const& tt, point_rtree const& loc_rtree, elevators const& e, elevator_footpath_map_t const& elevators_in_paths, platform_matches_t const& matches, nigiri::rt_timetable& rtt, std::chrono::seconds const max) { auto tasks = hash_set>{}; for (auto const& [e_in_path, from_to] : elevators_in_paths) { auto const e_idx = match_elevator(e.elevators_rtree_, e.elevators_, w, e_in_path); if (e_idx == elevator_idx_t::invalid()) { continue; } auto const& el = e.elevators_[e_idx]; if (el.out_of_service_.empty() && el.status_) { continue; } for (auto const& [from, to] : from_to) { tasks.emplace(from, osr::direction::kForward); tasks.emplace(to, osr::direction::kBackward); } } update_rtt_td_footpaths(w, l, pl, tt, loc_rtree, e, matches, tasks, nullptr, rtt, max); } } // namespace motis ================================================ FILE: test/combinations_test.cc ================================================ #include "gtest/gtest.h" #include "date/date.h" #include "nigiri/types.h" #include "motis/elevators/get_state_changes.h" using namespace date; using namespace std::chrono_literals; using namespace motis; namespace n = nigiri; std::ostream& operator<<(std::ostream& out, std::vector const& v) { auto first = true; for (auto const b : v) { if (!first) { out << ", "; } first = false; out << std::boolalpha << b; } return out; } TEST(motis, int_state_changes) { auto const changes = std::vector>>{ {{{0, true}, {10, false}, {20, true}}}, {{{0, false}, {5, true}, {10, false}, {15, true}, {20, false}, {25, true}, {30, false}}}}; auto g = get_state_changes(changes); auto const expected = std::array>, 7>{ std::pair>{0, {true, false}}, std::pair>{5, {true, true}}, std::pair>{10, {false, false}}, std::pair>{15, {false, true}}, std::pair>{20, {true, false}}, std::pair>{25, {true, true}}, std::pair>{30, {true, false}}}; auto i = 0U; while (g) { EXPECT_EQ(expected[i++], g()); } EXPECT_EQ(i, expected.size()); } ================================================ FILE: test/config_test.cc ================================================ #include "gtest/gtest.h" #include "motis/config.h" using namespace motis; using namespace std::string_literals; TEST(motis, config) { auto const c = config{ .osm_ = {"europe-latest.osm.pbf"}, .tiles_ = {{.profile_ = "deps/tiles/profile/profile.lua"}}, .timetable_ = {config::timetable{ .first_day_ = "2024-10-02", .num_days_ = 2U, .datasets_ = {{"de", {.path_ = "delfi.gtfs.zip", .clasz_bikes_allowed_ = {{{"LONG_DISTANCE", false}, {"REGIONAL_FAST", true}}}, .rt_ = {{{.url_ = R"(https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf)", .headers_ = {{{"Authorization", "test"}}}}}}}}, {"nl", {.path_ = "nl.gtfs.zip", .rt_ = {{{.url_ = R"(https://gtfs.ovapi.nl/nl/trainUpdates.pb)"}, {.url_ = R"(https://gtfs.ovapi.nl/nl/tripUpdates.pb)"}}}}}}, .assistance_times_ = {"assistance.csv"}}}, .street_routing_ = true, .limits_ = config::limits{}, .osr_footpath_ = true, .geocoding_ = true}; EXPECT_EQ(fmt::format(R"( osm: europe-latest.osm.pbf tiles: profile: deps/tiles/profile/profile.lua db_size: 274877906944 flush_threshold: 100000 timetable: first_day: 2024-10-02 num_days: 2 tb: false railviz: true with_shapes: true adjust_footpaths: true merge_dupes_intra_src: false merge_dupes_inter_src: false link_stop_distance: 100 update_interval: 60 http_timeout: 30 canned_rt: false incremental_rt_update: false use_osm_stop_coordinates: false extend_missing_footpaths: false max_footpath_length: 15 max_matching_distance: 25.000000 preprocess_max_matching_distance: 250.000000 datasets: de: path: delfi.gtfs.zip default_bikes_allowed: false default_cars_allowed: false extend_calendar: false clasz_bikes_allowed: LONG_DISTANCE: false REGIONAL_FAST: true rt: - url: https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf headers: Authorization: test protocol: gtfsrt nl: path: nl.gtfs.zip default_bikes_allowed: false default_cars_allowed: false extend_calendar: false rt: - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb protocol: gtfsrt - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb protocol: gtfsrt assistance_times: assistance.csv elevators: false street_routing: true limits: stoptimes_max_results: 256 plan_max_results: 256 plan_max_search_window_minutes: 5760 stops_max_results: 2048 onetomany_max_many: 128 onetoall_max_results: 65535 onetoall_max_travel_minutes: 90 routing_max_timeout_seconds: 90 gtfsrt_expose_max_trip_updates: 100 street_routing_max_prepost_transit_seconds: 3600 street_routing_max_direct_seconds: 21600 geocode_max_suggestions: 10 reverse_geocode_max_results: 5 osr_footpath: true geocoding: true reverse_geocoding: false )", std::thread::hardware_concurrency()), (std::stringstream{} << "\n" << c << "\n") .str()); EXPECT_EQ(c, config::read(R"( osm: europe-latest.osm.pbf tiles: profile: deps/tiles/profile/profile.lua timetable: first_day: 2024-10-02 num_days: 2 tb: false datasets: de: path: delfi.gtfs.zip clasz_bikes_allowed: LONG_DISTANCE: false REGIONAL_FAST: true rt: - url: https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf headers: Authorization: test nl: path: nl.gtfs.zip default_bikes_allowed: false default_cars_allowed: false extend_calendar: false rt: - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb assistance_times: assistance.csv elevators: false street_routing: true osr_footpath: true geocoding: true )"s)); EXPECT_TRUE(c.use_street_routing()); // Using street_routing struct { // Setting height_data_dir { auto const street_routing_config = config{.osm_ = {"europe-latest.osm.pbf"}, .street_routing_ = config::street_routing{.elevation_data_dir_ = "srtm/"}, .limits_ = config::limits{}}; EXPECT_EQ(street_routing_config, config::read(R"( street_routing: elevation_data_dir: srtm/ osm: europe-latest.osm.pbf )"s)); EXPECT_TRUE(street_routing_config.use_street_routing()); } // Using empty street_routing map { auto const street_routing_config = config{.osm_ = {"europe-latest.osm.pbf"}, .street_routing_ = config::street_routing{}, .limits_ = config::limits{}}; EXPECT_EQ(street_routing_config, config::read(R"( street_routing: {} osm: europe-latest.osm.pbf )"s)); EXPECT_TRUE(street_routing_config.use_street_routing()); } // No street_routing defined EXPECT_FALSE(config::read(R"( osm: europe-latest.osm.pbf )"s) .use_street_routing()); // street_routing disabled EXPECT_FALSE(config::read(R"( osm: europe-latest.osm.pbf street_routing: false )"s) .use_street_routing()); // Will throw if street_routing is set but osm is not EXPECT_ANY_THROW(config::read(R"( street_routing: {} )"s)); } } ================================================ FILE: test/elevators/parse_elevator_id_osm_mapping_test.cc ================================================ #include "gtest/gtest.h" #include #include "motis/elevators/parse_elevator_id_osm_mapping.h" using namespace motis; using namespace std::string_view_literals; constexpr auto const kElevatorIdOsmMappingCsv = R"__(dhid,diid,osm_kind,osm_id dbinfrago-temporary:d23340e4-ca1a-533e-803a-c036883147a3,diid:02b2be0f-c1da-1eef-a490-a02c488737ac,node,8891093860 de:01002:49320,diid:02b2be0f-c1da-1eef-a490-ddb6f99637ae,node,2505371425 de:01002:49320,diid:02b2be0f-c1da-1eef-a490-dfa6e17997ae,node,2505371422 de:01003:57774,diid:02b2be0f-c1da-1eef-a490-aec6aa7b37ad,node,2543654133 de:01004:66023,diid:02b2be0f-c1da-1eef-a490-a8aaa8ac17ac,node,3833491147 )__"sv; TEST(motis, parse_elevator_id_osm_mapping) { auto const map = parse_elevator_id_osm_mapping(kElevatorIdOsmMappingCsv); ASSERT_EQ(5, map.size()); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-a02c488737ac", map.at(8891093860ULL)); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-ddb6f99637ae", map.at(2505371425ULL)); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-dfa6e17997ae", map.at(2505371422ULL)); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-aec6aa7b37ad", map.at(2543654133ULL)); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-a8aaa8ac17ac", map.at(3833491147ULL)); } ================================================ FILE: test/elevators/parse_fasta_test.cc ================================================ #include "gtest/gtest.h" #include "motis/elevators/parse_fasta.h" using namespace date; using namespace std::chrono_literals; using namespace std::string_view_literals; using namespace motis; namespace n = nigiri; constexpr auto const kFastaJson = R"__( [ { "description": "FFM HBF zu Gleis 101/102 (S-Bahn)", "equipmentnumber" : 10561326, "geocoordX" : 8.6628995, "geocoordY" : 50.1072933, "operatorname" : "DB InfraGO", "state" : "ACTIVE", "stateExplanation" : "available", "stationnumber" : 1866, "type" : "ELEVATOR", "outOfService": [["2024-07-18T12:00:00Z", "2024-07-19T12:00:00Z"]] }, { "description": "FFM HBF zu Gleis 103/104 (S-Bahn)", "equipmentnumber": 10561327, "geocoordX": 8.6627516, "geocoordY": 50.1074549, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1866, "type": "ELEVATOR", "outOfService": [ ["2024-07-18T11:00:00Z", "2024-07-18T14:00:00Z"], ["2024-07-19T11:00:00Z", "2024-07-19T14:00:00Z"] ] }, { "description": "FFM HBF zu Gleis 103/104 (S-Bahn)", "equipmentnumber": 10561327, "geocoordX": 8.6627516, "geocoordY": 50.1074549, "operatorname": "DB InfraGO", "state": "INACTIVE", "stateExplanation": "available", "stationnumber": 1866, "type": "ELEVATOR" } ] )__"sv; TEST(motis, parse_fasta) { auto const elevators = parse_fasta(kFastaJson); ASSERT_EQ(3, elevators.size()); ASSERT_EQ(1, elevators[elevator_idx_t{0}].out_of_service_.size()); ASSERT_EQ(2, elevators[elevator_idx_t{1}].out_of_service_.size()); ASSERT_EQ(0, elevators[elevator_idx_t{2}].out_of_service_.size()); EXPECT_EQ((n::interval{sys_days{2024_y / July / 18} + 12h, sys_days{2024_y / July / 19} + 12h}), elevators[elevator_idx_t{0}].out_of_service_[0]); EXPECT_EQ((n::interval{sys_days{2024_y / July / 18} + 11h, sys_days{2024_y / July / 18} + 14h}), elevators[elevator_idx_t{1}].out_of_service_[0]); EXPECT_EQ((n::interval{sys_days{2024_y / July / 19} + 11h, sys_days{2024_y / July / 19} + 14h}), elevators[elevator_idx_t{1}].out_of_service_[1]); EXPECT_EQ((std::vector>{ {n::unixtime_t{n::unixtime_t::duration{0}}, true}, {sys_days{2024_y / July / 18} + 12h, false}, {sys_days{2024_y / July / 19} + 12h, true}}), elevators[elevator_idx_t{0}].state_changes_); EXPECT_EQ((std::vector>{ {n::unixtime_t{n::unixtime_t::duration{0}}, true}, {sys_days{2024_y / July / 18} + 11h, false}, {sys_days{2024_y / July / 18} + 14h, true}, {sys_days{2024_y / July / 19} + 11h, false}, {sys_days{2024_y / July / 19} + 14h, true}}), elevators[elevator_idx_t{1}].state_changes_); auto const expected = std::array>, 7>{ std::pair>{ n::unixtime_t{n::unixtime_t::duration{0}}, {true, true, false}}, std::pair>{ sys_days{2024_y / July / 18} + 11h, {true, false, false}}, std::pair>{ sys_days{2024_y / July / 18} + 12h, {false, false, false}}, std::pair>{ sys_days{2024_y / July / 18} + 14h, {false, true, false}}, std::pair>{ sys_days{2024_y / July / 19} + 11h, {false, false, false}}, std::pair>{ sys_days{2024_y / July / 19} + 12h, {true, false, false}}, std::pair>{ sys_days{2024_y / July / 19} + 14h, {true, true, false}}}; auto const single_state_changes = utl::to_vec( elevators, [&](elevator const& e) { return e.get_state_changes(); }); auto g = get_state_changes(single_state_changes); auto i = 0U; while (g) { auto const x = g(); ASSERT_LT(i, expected.size()); EXPECT_EQ(expected[i], x) << "i=" << i; ++i; } EXPECT_EQ(expected.size(), i); } ================================================ FILE: test/elevators/parse_siri_fm_test.cc ================================================ #include "gtest/gtest.h" #include #include "motis/elevators/parse_siri_fm.h" using namespace motis; constexpr auto kSiriFm = R"( 2026-02-21T22:06:02Z dbinfrago 2026-02-21T22:06:02Z diid:02aeed9c-f8a3-1fd0-bceb-153a94e98000 unknown not monitored diid:02b2be0f-c1da-1eef-a490-d36ed2aeb7ae available available diid:02b2be0f-c1da-1eef-a490-a458663cb7ac available available diid:02b2be0f-c1da-1eef-a490-9efb1b6717ac available available diid:02b2be0f-c1da-1eef-a490-9d577a00b7ac available available diid:02b2be0f-c1da-1eef-a490-b4967b7cf7ae available available diid:02b2be0f-c1da-1eef-a490-db549df337ae notAvailable not available )"; TEST(motis, parse_siri_fm) { auto const elevators = parse_siri_fm(std::string_view{kSiriFm}); ASSERT_EQ(7, elevators.size()); ASSERT_TRUE(elevators[elevator_idx_t{0}].id_str_.has_value()); ASSERT_TRUE(elevators[elevator_idx_t{1}].id_str_.has_value()); ASSERT_TRUE(elevators[elevator_idx_t{2}].id_str_.has_value()); ASSERT_TRUE(elevators[elevator_idx_t{3}].id_str_.has_value()); ASSERT_TRUE(elevators[elevator_idx_t{4}].id_str_.has_value()); ASSERT_TRUE(elevators[elevator_idx_t{5}].id_str_.has_value()); ASSERT_TRUE(elevators[elevator_idx_t{6}].id_str_.has_value()); EXPECT_EQ("diid:02aeed9c-f8a3-1fd0-bceb-153a94e98000", *elevators[elevator_idx_t{0}].id_str_); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-d36ed2aeb7ae", *elevators[elevator_idx_t{1}].id_str_); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-a458663cb7ac", *elevators[elevator_idx_t{2}].id_str_); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-9efb1b6717ac", *elevators[elevator_idx_t{3}].id_str_); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-9d577a00b7ac", *elevators[elevator_idx_t{4}].id_str_); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-b4967b7cf7ae", *elevators[elevator_idx_t{5}].id_str_); EXPECT_EQ("diid:02b2be0f-c1da-1eef-a490-db549df337ae", *elevators[elevator_idx_t{6}].id_str_); EXPECT_FALSE(elevators[elevator_idx_t{0}].status_); EXPECT_TRUE(elevators[elevator_idx_t{1}].status_); EXPECT_TRUE(elevators[elevator_idx_t{2}].status_); EXPECT_TRUE(elevators[elevator_idx_t{3}].status_); EXPECT_TRUE(elevators[elevator_idx_t{4}].status_); EXPECT_TRUE(elevators[elevator_idx_t{5}].status_); EXPECT_FALSE(elevators[elevator_idx_t{6}].status_); for (auto const& e : elevators) { EXPECT_TRUE(e.id_str_.has_value()); EXPECT_TRUE(e.out_of_service_.empty()); EXPECT_EQ(1, e.state_changes_.size()); EXPECT_EQ(e.status_, e.state_changes_.front().state_); } } ================================================ FILE: test/elevators/siri_fm_routing_test.cc ================================================ #include "gtest/gtest.h" #include #ifdef NO_DATA #undef NO_DATA #endif #include "gtfsrt/gtfs-realtime.pb.h" #include "utl/init_from.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/routing.h" #include "motis/import.h" namespace json = boost::json; using namespace std::string_view_literals; using namespace motis; using namespace date; using namespace std::chrono_literals; namespace n = nigiri; constexpr auto const kElevatorIdOsm = R"(dhid,diid,osm_kind,osm_id de:06412:10,diid:02b2be0f-c1da-1eef-a490-d5f7573837ae,node,3945358489 )"; constexpr auto const kSiriFm = R"__( 2026-02-21T22:06:02Z dbinfrago 2026-02-21T22:06:02Z diid:02b2be0f-c1da-1eef-a490-d5f7573837ae notAvailable not available )__"sv; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3,DB,S3,,,109 U4,DB,U4,,,402 ICE,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3,S1,S3,, U4,S1,U4,, ICE,S1,ICE,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type S3,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0 U4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 U4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0 ICE,00:35:00,00:35:00,DA_10,0,0,0 ICE,00:45:00,00:45:00,FFM_10,1,0,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 # frequencies.txt trip_id,start_time,end_time,headway_secs S3,01:15:00,25:15:00,3600 ICE,00:35:00,24:35:00,3600 U4,01:05:00,25:01:00,3600 )"sv; void print_short(std::ostream& out, api::Itinerary const& j); std::string to_str(std::vector const& x); TEST(motis, siri_fm_routing) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{.server_ = {{.web_folder_ = "ui/build", .n_threads_ = 1U}}, .osm_ = {"test/resources/test_case.osm.pbf"}, .tiles_ = {{.profile_ = "deps/tiles/profile/full.lua", .db_size_ = 1024U * 1024U * 25U}}, .timetable_ = config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .preprocess_max_matching_distance_ = 0.0, .datasets_ = {{"test", {.path_ = std::string{kGTFS}}}}}, .elevators_ = config::elevators{.init_ = std::string{kSiriFm}, .osm_mapping_ = std::string{kElevatorIdOsm}}, .street_routing_ = true, .osr_footpath_ = true, .geocoding_ = true, .reverse_geocoding_ = true}; import(c, "test/data"); auto d = data{"test/data", c}; auto const routing = utl::init_from(d).value(); // Route with wheelchair. { auto const res = routing( "?fromPlace=49.87263,8.63127" "&toPlace=50.11347,8.67664" "&time=2019-05-01T01:25Z" "&pedestrianProfile=WHEELCHAIR" "&useRoutedTransfers=true" "&timetableView=false"); EXPECT_EQ(0U, res.itineraries_.size()); } // Route w/o wheelchair. { auto const res = routing( "?fromPlace=49.87263,8.63127" "&toPlace=50.11347,8.67664" "&time=2019-05-01T01:25Z" "&useRoutedTransfers=true" "&timetableView=false"); EXPECT_EQ(1U, res.itineraries_.size()); } } ================================================ FILE: test/endpoints/map_routes_test.cc ================================================ #include "gmock/gmock-matchers.h" #include "gtest/gtest.h" #include #include #include "boost/json.hpp" #include "net/bad_request_exception.h" #include "net/not_found_exception.h" #include "utl/init_from.h" #include "motis-api/motis-api.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/map/routes.h" #include "motis/gbfs/update.h" #include "motis/import.h" namespace json = boost::json; using namespace std::string_view_literals; using namespace motis; using namespace date; using namespace std::chrono_literals; using namespace testing; namespace n = nigiri; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone Test,Test,https://example.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon DA_Bus_1,DA Hbf,49.8724891,8.6281994 DA_Bus_2,DA Hbf,49.8750407,8.6312172 DA_Tram_1,DA Hbf,49.8742551,8.6321063 DA_Tram_2,DA Hbf,49.8731133,8.6313674 DA_Tram_3,DA Hbf,49.872435,8.632164 # routes.txt route_id,agency_id,route_short_name,route_long_name,route_type B1,Test,B1,,3 T1,Test,T1,,0 # trips.txt route_id,service_id,trip_id,trip_headsign B1,S1,B1,Bus 1, T1,S1,T1,Tram 1, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence B1,01:00:00,01:00:00,DA_Bus_1,1 B1,01:10:00,01:10:00,DA_Bus_2,2 T1,01:05:00,01:05:00,DA_Tram_1,1 T1,01:15:00,01:15:00,DA_Tram_2,2 T1,01:20:00,01:20:00,DA_Tram_3,3 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 )"; TEST(motis, map_routes) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{ .osm_ = {"test/resources/test_case.osm.pbf"}, .timetable_ = config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .with_shapes_ = true, .datasets_ = {{"test", {.path_ = kGTFS}}}, .route_shapes_ = {{.mode_ = config::timetable::route_shapes::mode::all, .cache_db_size_ = 1024U * 1024U * 5U}}}, .street_routing_ = true}; import(c, "test/data"); auto d = data{"test/data", c}; auto const map_routes = utl::init_from(d).value(); { auto const res = map_routes( "/api/experimental/map/routes" "?max=49.88135900212875%2C8.60917200508915" "&min=49.863844157325886%2C8.649823169526556" "&zoom=16"); EXPECT_EQ(res.routes_.size(), 2U); EXPECT_EQ(res.zoomFiltered_, false); EXPECT_THAT(res.routes_, Contains(Field(&api::RouteInfo::mode_, Eq(api::ModeEnum::BUS)))); EXPECT_THAT(res.routes_, Contains(Field(&api::RouteInfo::mode_, Eq(api::ModeEnum::TRAM)))); EXPECT_THAT(res.routes_, Each(Field(&api::RouteInfo::pathSource_, Eq(api::RoutePathSourceEnum::ROUTED)))); EXPECT_FALSE(res.polylines_.empty()); EXPECT_FALSE(res.stops_.empty()); for (auto const& route : res.routes_) { for (auto const& segment : route.segments_) { EXPECT_GE(segment.from_, 0); EXPECT_GE(segment.to_, 0); EXPECT_GE(segment.polyline_, 0); EXPECT_LT(segment.from_, static_cast(res.stops_.size())); EXPECT_LT(segment.to_, static_cast(res.stops_.size())); EXPECT_LT(segment.polyline_, static_cast(res.polylines_.size())); } } for (auto route_index = 0U; route_index != res.routes_.size(); ++route_index) { for (auto const& segment : res.routes_[route_index].segments_) { EXPECT_THAT( res.polylines_.at(static_cast(segment.polyline_)) .routeIndexes_, Contains(static_cast(route_index))); } } } { // map section without data auto const res = map_routes( "/api/experimental/map/routes" "?max=53.5757876577963%2C9.904453881311966" "&min=53.518462458295005%2C10.04877290275494" "&zoom=14.5"); EXPECT_EQ(res.routes_.size(), 0U); EXPECT_EQ(res.zoomFiltered_, false); } } ================================================ FILE: test/endpoints/ojp_test.cc ================================================ #include "gtest/gtest.h" #include #include #include #include #include #include "date/date.h" #include "utl/init_from.h" #include "utl/read_file.h" #include "adr/formatter.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/ojp.h" #include "motis/import.h" using namespace motis; using namespace date; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3,DB,S3,,,109 U4,DB,U4,,,402 ICE,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3,S1,S3,, U4,S1,U4,, ICE,S1,ICE,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type S3,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0 U4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 U4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0 ICE,00:35:00,00:35:00,DA_10,0,0,0 ICE,00:45:00,00:45:00,FFM_10,1,0,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 # frequencies.txt trip_id,start_time,end_time,headway_secs S3,01:15:00,25:15:00,3600 ICE,00:35:00,24:35:00,3600 U4,01:05:00,25:01:00,3600 )"; TEST(motis, ojp_requests) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{.osm_ = {"test/resources/test_case.osm.pbf"}, .timetable_ = config::timetable{.first_day_ = "2019-05-01", .num_days_ = 2, .datasets_ = {{"test", {.path_ = kGTFS}}}}, .street_routing_ = true, .osr_footpath_ = true, .geocoding_ = true}; import(c, "test/data"); auto d = data{"test/data", c}; d.init_rtt(date::sys_days{2019_y / May / 1}); auto const ojp_ep = ep::ojp{ .routing_ep_ = utl::init_from(d), .geocoding_ep_ = utl::init_from(d), .stops_ep_ = utl::init_from(d), .stop_times_ep_ = utl::init_from(d), .trip_ep_ = utl::init_from(d), }; auto const send_request = [&](std::string_view body) { net::web_server::http_req_t req{boost::beast::http::verb::post, "/api/v2/ojp", 11}; req.set(boost::beast::http::field::content_type, "text/xml; charset=utf-8"); req.body() = std::string{body}; req.prepare_payload(); return ojp_ep(net::route_request{std::move(req)}, false); }; auto const normalize_response = [](std::string_view input) { auto out = std::string{input}; auto const normalize_tag = [&](std::string_view start_tag, std::string_view end_tag, std::string_view replacement) { auto pos = std::size_t{0}; while ((pos = out.find(start_tag, pos)) != std::string::npos) { auto const value_start = pos + start_tag.size(); auto const value_end = out.find(end_tag, value_start); if (value_end == std::string::npos) { break; } out.replace(value_start, value_end - value_start, replacement); pos = value_start + replacement.size() + end_tag.size(); } }; normalize_tag("", "", "NOW"); normalize_tag("", "", "MSG"); normalize_tag("", "", ""); return out; }; auto const expect_response = [&](char const* request_path, char const* response_path) { auto const request = utl::read_file(request_path).value(); auto expected = utl::read_file(response_path).value(); auto const reply = send_request(request); auto const* res = std::get_if(&reply); ASSERT_NE(nullptr, res); EXPECT_EQ(boost::beast::http::status::ok, res->result()); EXPECT_EQ("text/xml; charset=utf-8", res->base()[boost::beast::http::field::content_type]); EXPECT_EQ(normalize_response(expected), normalize_response(res->body())); }; expect_response("test/resources/ojp/geocoding_request.xml", "test/resources/ojp/geocoding_response.xml"); expect_response("test/resources/ojp/map_stops_request.xml", "test/resources/ojp/map_stops_response.xml"); expect_response("test/resources/ojp/stop_event_request.xml", "test/resources/ojp/stop_event_response.xml"); expect_response("test/resources/ojp/trip_info_request.xml", "test/resources/ojp/trip_info_response.xml"); expect_response("test/resources/ojp/routing_request.xml", "test/resources/ojp/routing_response.xml"); expect_response("test/resources/ojp/intermodal_routing_request.xml", "test/resources/ojp/intermodal_routing_response.xml"); } ================================================ FILE: test/endpoints/one_to_many_test.cc ================================================ #include "gtest/gtest.h" #ifdef NO_DATA #undef NO_DATA #endif #include #include #include #include "utl/init_from.h" #include "nigiri/common/parse_time.h" #include "motis-api/motis-api.h" #include "motis/config.h" #include "motis/endpoints/one_to_many.h" #include "motis/endpoints/one_to_many_post.h" #include "../test_case.h" using namespace std::string_view_literals; using namespace motis; using namespace date; using namespace std::chrono_literals; namespace n = nigiri; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, PAUL1,Römer/Paulskirche,50.110979,8.682276,0,, PAUL2,Römer/Paulskirche,50.110828,8.681587,0,, FFM_C,FFM C,50.107812,8.664628,0,, FFM_B,FFM B,50.107519,8.664775,0,, DA_Bus_1,DA Hbf,49.8722160,8.6282315 DA_Bus_2,DA Hbf,49.8755778,8.6240518 DA_Tram_1,DA Hbf,49.8752926,8.6277460 DA_Tram_2,DA Hbf,49.874995,8.6313925 DA_Tram_3,DA Hbf,49.871561,8.6320181 # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3,DB,S3,,,109 U4,DB,U4,,,402 ICE,DB,ICE,,,101 11_1,DB,11,,,0 11_2,DB,11,,,0 B1,DB,B1,,3 T1,DB,T1,,0 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3,S1,S3,,block_1 U4,S1,U4,,block_1 ICE,S1,ICE,, 11_1,S1,11_1_1,, 11_1,S1,11_1_2,, 11_2,S1,11_2_1,, 11_2,S1,11_2_2,, B1,S1,B1,Bus 1, T1,S1,T1,Tram 1, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type S3,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0 U4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 U4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0 ICE,00:35:00,00:35:00,DA_10,0,0,0 ICE,00:45:00,00:45:00,FFM_10,1,0,0 11_1_1,12:00:00,12:00:00,PAUL1,0,0,0 11_1_1,12:10:00,12:10:00,FFM_C,1,0,0 11_1_2,12:15:00,12:15:00,PAUL1,0,0,0 11_1_2,12:25:00,12:25:00,FFM_C,1,0,0 11_2_1,12:05:00,12:05:00,FFM_B,0,0,0 11_2_1,12:15:00,12:15:00,PAUL2,1,0,0 11_2_2,12:20:00,12:20:00,FFM_B,0,0,0 11_2_2,12:30:00,12:30:00,PAUL2,1,0,0 B1,00:10:00,00:10:00,DA_Bus_1,1 B1,00:20:00,00:20:00,DA_Bus_2,2 T1,00:24:00,00:24:00,DA_Tram_1,1 T1,00:25:00,00:25:00,DA_Tram_2,2 T1,00:26:00,00:26:00,DA_Tram_3,3 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 )"; template <> test_case_params const import_test_case() { auto const c = config{.osm_ = {"test/resources/test_case.osm.pbf"}, .timetable_ = config::timetable{.first_day_ = "2019-05-01", .num_days_ = 2, .datasets_ = {{"test", {.path_ = kGTFS}}}}, .street_routing_ = true, .osr_footpath_ = true}; return import_test_case(std::move(c), "test/test_case/ffm_one_to_many_data"); } std::chrono::time_point parse_time(std::string_view time) { return std::chrono::time_point_cast( n::parse_time(time, "%FT%T%Ez")); } auto one_to_many_get(data& d) { return utl::init_from(d).value(); } auto one_to_many_post(data& d) { return utl::init_from(d).value(); } TEST(one_to_many, get_request_forward) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_get(d)( "/api/experimental/one-to-many-intermodal" "?one=49.8722439;8.6320624" // Near DA "&many=" "49.87336;8.62926," // DA_10 "50.10593;8.66118," // FFM_10 "50.107577;8.6638173," // de:6412:10:6:1 "50.10739;8.66333," // FFM_101 "50.11385;8.67912," // FFM_HAUPT_U "50.11404;8.67824," // FFM_HAUPT_S "49.872855;8.632008," // Near one "49.872504;8.628988," // Inside DA station "49.874399;8.630361," // Still reachable, near DA "49.875292;8.627746," // Far, near DA "50.106596;8.663485," // Inside FFM station "50.106209;8.668934," // Outside FFM station "50.103663;8.663666," // Far, not reachable, near FFM "50.113494;8.679129," // Near FFM_HAUPT_U "50.114080;8.677027," // Near FFM_HAUPT_S "50.114520;8.673050," // Too far from FFM_HAUPT_U "50.114773;8.672604" // Far, near FFM_HAUPT "&time=2019-04-30T22:30:00.000Z" "&maxTravelTime=60" "&maxMatchingDistance=250" "&maxDirectTime=540" // Updated to maxPreTransitTime == 1500 "&maxPostTransitTime=420" "&arriveBy=false"); EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = {{{.duration_ = 281.0}, {}, {}, {}, {}, {}, {.duration_ = 122.0}, {.duration_ = 240.0}, {.duration_ = 529.0}, {.duration_ = 582.0}, {}, {}, {}, {}, {}, {}, {}}}, .transit_durations_ = {{ {}, {{.duration_ = 1080.0, .transfers_ = 0}}, {// Not routed transfer => faster than realistic {.duration_ = 1140.0, .transfers_ = 0}}, {// Not routed transfer {.duration_ = 1140.0, .transfers_ = 0}}, {{.duration_ = 2580.0, .transfers_ = 1}}, {{.duration_ = 2580.0, .transfers_ = 1}}, {}, {}, {}, {}, {{.duration_ = 1260.0, .transfers_ = 0}}, {{.duration_ = 1620.0, .transfers_ = 0}}, {}, {{.duration_ = 2700.0, .transfers_ = 1}}, {{.duration_ = 2640.0, .transfers_ = 1}}, {{.duration_ = 2940.0, .transfers_ = 1}}, {}, }}}), durations); } TEST(one_to_many, post_request_backward) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "50.113816,8.679421,0", // Near FFM_HAUPT .many_ = { "49.87336,8.62926", // DA_10 "50.10593,8.66118", // FFM_10 "test_FFM_10", "50.107577,8.6638173", // de:6412:10:6:1 "50.10739,8.66333", // FFM_101 "test_FFM_101", "50.11385,8.67912", // FFM_HAUPT_U "50.11385,8.67912,-4", // FFM_HAUPT_U "test_FFM_HAUPT_U", "50.11404,8.67824", // FFM_HAUPT_S "50.113385,8.678328,0", // Close, near FFM_HAUPT, level 0 "50.113385,8.678328,-2", // Close, near FFM_HAUPT, level -2 "50.111900,8.675208", // Far, near FFM_HAUPT "50.106543,8.663474,0", // Close, near FFM "50.107361,8.660478", // Too far from de:6412:10:6:1 "50.104298,8.660285", // Far, near FFM "49.872243,8.632062", // Near DA "49.875368,8.627596", // Far, near DA }, .time_ = parse_time("2019-05-01T01:25:00.000+02:00"), .maxTravelTime_ = 60, .maxMatchingDistance_ = 250.0, .arriveBy_ = true, .maxPreTransitTime_ = 300, .maxDirectTime_ = 300}); // Updated to maxPostTransitTime == 1500 EXPECT_EQ( (api::OneToManyIntermodalResponse{ .street_durations_ = {{ {}, {}, {}, {}, {}, {}, {.duration_ = 159.0}, // No explicit level {.duration_ = 160.0}, // Explicit level {.duration_ = 160.0}, {.duration_ = 127.0}, {.duration_ = 103.0}, {.duration_ = 123.0}, {.duration_ = 355.0}, {}, {}, {}, {}, {}, }}, .transit_durations_ = {{ {{.duration_ = 3180.0, .transfers_ = 1}}, {{.duration_ = 840.0, .transfers_ = 0}}, // Not routed transfer {{.duration_ = 720.0, .transfers_ = 0}}, // Not routed transfer {{.duration_ = 780.0, .transfers_ = 0}}, {{.duration_ = 780.0, .transfers_ = 0}}, {{.duration_ = 720.0, .transfers_ = 0}}, {}, {}, {}, {}, {}, {}, {}, {{.duration_ = 900.0, .transfers_ = 0}}, {{.duration_ = 1020.0, .transfers_ = 0}}, {}, {{.duration_ = 3360.0, .transfers_ = 1}}, // from DA_10: 3420.0 {}, }}}), durations); } TEST(one_to_many, post_request_forward_with_routed_transfers_and_short_pre_transit) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "50.106691,8.659763", // Near FFM .many_ = { "test_DA_10", "50.107577,8.6638173", // de:6412:10:6:1 "test_de:6412:10:6:1", "test_FFM_101", "test_FFM_HAUPT_S", "50.11385,8.67912", // FFM_HAUPT_U "50.10590,8.66452", // Near FFM "50.113291,8.678321,0", // Near FFM_HAUPT "50.113127,8.678879,-2", // Near FFM_HAUPT "50.114141,8.677025,-3", // Near FFM_HAUPT "50.113589,8.679070,-4", // Near FFM_HAUPT }, .time_ = parse_time("2019-05-01T00:55:00.000+02:00"), .maxTravelTime_ = 60, .maxMatchingDistance_ = 250.0, .arriveBy_ = false, .useRoutedTransfers_ = true, .maxPreTransitTime_ = 360}); // Too short to reach U4 EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = {{ {}, {.duration_ = 443.0}, {.duration_ = 370.0}, // Direct connection allowed {.duration_ = 321.0}, // Valid for pre transit {}, {}, {.duration_ = 403.0}, {}, {}, {}, {}, }}, .transit_durations_ = {{ {}, {}, {}, {}, {{.duration_ = 1560.0, .transfers_ = 0}}, // Must take S3 {{.duration_ = 1680.0, .transfers_ = 0}}, // Must take S3 {}, {{.duration_ = 1800.0, .transfers_ = 0}}, {{.duration_ = 1740.0, .transfers_ = 0}}, {{.duration_ = 1740.0, .transfers_ = 0}}, {{.duration_ = 1680.0, .transfers_ = 0}}, }}}), durations); } TEST(one_to_many, get_request_backward_with_wheelchair_and_short_post_transit) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_get(d)( "/api/experimental/one-to-many-intermodal" "?one=50.11385;8.67912" // FFM_HAUPT_U "&many=" "50.107577;8.6638173," // de:6412:10:6:1 "50.10739;8.66333," // FFM_101 "50.11404;8.67824," // FFM_HAUPT_S "50.113465;8.678477," // Near FFM_HAUPT "50.112519;8.676565" // Far, near FFM_HAUPT "&time=2019-04-30T23:30:00.000Z" "&maxTravelTime=60" "&maxMatchingDistance=250" "&maxDirectTime=540" "&maxPostTransitTime=240" "&pedestrianProfile=WHEELCHAIR" "&useRoutedTransfers=true" "&withDistance=true" "&arriveBy=true"); auto const& sd = durations.street_durations_.value(); auto const& td = durations.transit_durations_.value(); ASSERT_EQ(5U, sd.size()); EXPECT_EQ(api::Duration{}, sd.at(0)); EXPECT_EQ(api::Duration{}, sd.at(1)); // Not valid for post transit => unreachable from FFM_101 EXPECT_DOUBLE_EQ(333.0, sd.at(2).duration_.value()); EXPECT_NEAR(124.1, sd.at(2).distance_.value(), 0.1); EXPECT_DOUBLE_EQ(517.0, sd.at(3).duration_.value()); EXPECT_NEAR(271.8, sd.at(3).distance_.value(), 0.1); EXPECT_DOUBLE_EQ(771.0, sd.at(4).duration_.value()); EXPECT_NEAR(476.0, sd.at(4).distance_.value(), 0.1); ASSERT_EQ(5U, td.size()); ASSERT_EQ(1U, td.at(0).size()); EXPECT_DOUBLE_EQ(1680.0, td.at(0).at(0).duration_); EXPECT_EQ(0, td.at(0).at(0).transfers_); // Unreachable, as FFM_HAUPT_S -> FFM_HAUPT_U not usable postTransit EXPECT_TRUE(td.at(1).empty()); EXPECT_TRUE(td.at(2).empty()); EXPECT_TRUE(td.at(3).empty()); EXPECT_TRUE(td.at(4).empty()); } TEST(one_to_many, oneway_get_forward_for_pre_transit_and_direct_modes) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_get(d)( "/api/experimental/one-to-many-intermodal" "?one=50.107328;8.664836" "&many=" "50.107812;8.664628," // FFM C (shorter path) "50.107519;8.664775," // FFM B (longer path, due to oneway) "50.110828;8.681587" // PAUL2 "&time=2019-05-01T10:00:00.00Z" "&maxTravelTime=60" "&maxMatchingDistance=250" "&maxDirectTime=3600" "&directMode=BIKE" "&preTransitModes=BIKE" "&arriveBy=false" "&cyclingSpeed=2.4"); EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = {{ {.duration_ = 228.0}, {.duration_ = 321.0}, {}, }}, .transit_durations_ = {{ {}, {}, {// Must use later trip {.duration_ = 1980.0, .transfers_ = 0}}, }}}), durations); } TEST(one_to_many, oneway_post_backward_for_post_transit_and_direct_modes) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "50.107326,8.665237", .many_ = {"test_FFM_B", "test_FFM_C", "50.107812,8.664628", "test_PAUL1"}, .time_ = parse_time("2019-05-01T12:30:00.000+02:00"), .maxTravelTime_ = 60, .maxMatchingDistance_ = 250.0, .arriveBy_ = true, .cyclingSpeed_ = 2.2, .postTransitModes_ = {api::ModeEnum::BIKE}, .directMode_ = api::ModeEnum::BIKE, .withDistance_ = true}); auto const& sd = durations.street_durations_.value(); auto const& td = durations.transit_durations_.value(); ASSERT_EQ(4U, sd.size()); EXPECT_DOUBLE_EQ(228.0, sd.at(0).duration_.value()); EXPECT_NEAR(341.3, sd.at(0).distance_.value(), 0.1); EXPECT_DOUBLE_EQ(335.0, sd.at(1).duration_.value()); EXPECT_NEAR(502.1, sd.at(1).distance_.value(), 0.1); EXPECT_DOUBLE_EQ(335.0, sd.at(2).duration_.value()); EXPECT_NEAR(502.1, sd.at(2).distance_.value(), 0.1); EXPECT_EQ(api::Duration{}, sd.at(3)); ASSERT_EQ(4U, td.size()); EXPECT_TRUE(td.at(0).empty()); EXPECT_TRUE(td.at(1).empty()); EXPECT_TRUE(td.at(2).empty()); ASSERT_EQ(1U, td.at(3).size()); EXPECT_DOUBLE_EQ(1920.0, td.at(3).at(0).duration_); EXPECT_EQ(0, td.at(3).at(0).transfers_); } TEST(one_to_many, oneway_post_forward_for_post_transit_modes) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "test_PAUL1", .many_ = {"test_FFM_C", "50.107326,8.665237"}, // includes C -> B .time_ = parse_time("2019-05-01T12:00:00.000+02:00"), .maxTravelTime_ = 30, .maxMatchingDistance_ = 250.0, .arriveBy_ = false, .postTransitModes_ = {api::ModeEnum::BIKE}}); EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = std::vector(2), .transit_durations_ = {{ {{.duration_ = 720.0, .transfers_ = 0}}, {{.duration_ = 840.0, .transfers_ = 0}}, }}}), durations); } TEST(one_to_many, oneway_get_backward_for_pre_transit_modes) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_get(d)( "/api/experimental/one-to-many-intermodal" "?one=50.110828;8.681587" // PAUL2 "&many=" "50.107812;8.664628," // FFM C (with incorrect transfer C -> B) "50.107519;8.664775," // FFM B "50.107328;8.664836" // Long preTransit due to oneway (C -> B) "&time=2019-05-01T10:20:00.00Z" "&maxTravelTime=60" "&maxMatchingDistance=250" "&preTransitModes=BIKE" "&arriveBy=true" "&cyclingSpeed=2.4"); EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = std::vector(3), .transit_durations_ = {{ {{.duration_ = 1080.0, .transfers_ = 0}}, {{.duration_ = 1080.0, .transfers_ = 0}}, {{.duration_ = 1260.0, .transfers_ = 0}}, }}}), durations); } TEST(one_to_many, transfer_time_settings_min_transfer_time) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_get(d)( "/api/experimental/one-to-many-intermodal" "?one=49.872710;8.631168" // Near DA "&many=50.113487;8.678913" // Near FFM_HAUPT "&time=2019-04-30T22:30:00.00Z" "&useRoutedTransfers=true" "&minTransferTime=21"); // FIXME Times are also added for final footpath EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = {{api::Duration{}}}, .transit_durations_ = {{ {{.duration_ = 4320.0, .transfers_ = 1}}, }}}), durations); } TEST(one_to_many, transfer_time_settings_additional_transfer_time) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "49.872710, 8.631168", // Near DA .many_ = {"50.113487, 8.678913"}, // Near FFM_HAUPT .time_ = parse_time("2019-05-01T00:30:00.000+02:00"), .additionalTransferTime_ = 17, .useRoutedTransfers_ = true}); // FIXME Times are also added for final footpath EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = {{api::Duration{}}}, .transit_durations_ = {{ {{.duration_ = 4200.0, .transfers_ = 1}}, }}}), durations); } TEST(one_to_many, bug_additional_footpath_for_first_last_mile) { // Bug examples: Should not connect final footpath with first or last mile auto [d, _config] = get_test_case(); auto const ep = one_to_many_post(d); auto const many = std::vector{ "test_FFM_HAUPT_U", "50.11385,8.67912,-4", // FFM_HAUPT_U "test_FFM_HAUPT_S", "50.11404,8.67824,-3", // FFM_HAUPT_S "50.114093,8.676546"}; // Test location { // Last location should not be reachable when only arriving with U4 auto const durations = ep(api::OneToManyIntermodalParams{ .one_ = "50.108056,8.663177,-2", // Near de:6412:10:6:1 .many_ = many, .time_ = parse_time("2019-05-01T01:00:00.000+02:00"), .maxTravelTime_ = 30, .maxMatchingDistance_ = 250.0, .arriveBy_ = false, .useRoutedTransfers_ = true, .pedestrianProfile_ = api::PedestrianProfileEnum::WHEELCHAIR, .maxPostTransitTime_ = 420}); // Too short to reach from U4 EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = std::vector(5), .transit_durations_ = {{ {{.duration_ = 720.0, .transfers_ = 0}}, {{.duration_ = 780.0, .transfers_ = 0}}, {{.duration_ = 720.0, .transfers_ = 0}}, {{.duration_ = 1020.0, .transfers_ = 0}}, {// FIXME Test location should be unreachable {.duration_ = 1380.0, .transfers_ = 0}}, }}}), durations); } { // Test that location is reachable from FFM_HAUPT_S after arrival auto const test_durations = ep(api::OneToManyIntermodalParams{ .one_ = "50.10739,8.66333,-3", // Near FFM_101 .many_ = many, .time_ = parse_time("2019-05-01T01:00:00.000+02:00"), .maxTravelTime_ = 30, .maxMatchingDistance_ = 250.0, .arriveBy_ = false, .useRoutedTransfers_ = true, .pedestrianProfile_ = api::PedestrianProfileEnum::WHEELCHAIR, .maxPostTransitTime_ = 420}); // Reachable from S3 EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = std::vector(5), .transit_durations_ = {{ {{.duration_ = 1260.0, .transfers_ = 0}}, {{.duration_ = 1620.0, .transfers_ = 0}}, {{.duration_ = 1260.0, .transfers_ = 0}}, {{.duration_ = 1380.0, .transfers_ = 0}}, {{.duration_ = 1740.0, .transfers_ = 0}}, }}}), test_durations); } { // Ensure all arriving stations are reachable // Also check that the correct arrival time is used // Should start from FFM_HAUPT_S due to post transit time constraint auto const walk_durations = ep(api::OneToManyIntermodalParams{ .one_ = "50.107066,8.663604,0", .many_ = many, .time_ = parse_time("2019-05-01T01:00:00.000+02:00"), .maxTravelTime_ = 30, .maxMatchingDistance_ = 250.0, .arriveBy_ = false, .useRoutedTransfers_ = true, .pedestrianProfile_ = api::PedestrianProfileEnum::FOOT, .maxPostTransitTime_ = 240}); // Only reachable from S3 EXPECT_EQ((api::OneToManyIntermodalResponse{ .street_durations_ = std::vector(5), .transit_durations_ = {{ {{.duration_ = 720.0, .transfers_ = 0}}, {{.duration_ = 780.0, .transfers_ = 0}}, {{.duration_ = 720.0, .transfers_ = 0}}, {{.duration_ = 780.0, .transfers_ = 0}}, {// FIXME Should start FFM_HAUPT_S => time > 1200 {.duration_ = 960.0, .transfers_ = 0}}, }}}), walk_durations); } } TEST(one_to_many, pareto_sets_with_routed_transfers_and_distances) { auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "49.8722160,8.6282315", // DA_Bus_1 .many_ = {"49.875292,8.6277460", // near Tram_1, must be south of node "49.874995,8.6313925", // near Tram_2 "49.871561,8.6320181", // near Tram_3 "50.111900,8.675208"}, // near FFM_HAUPT .time_ = parse_time("2019-05-01T00:05:00.000+02:00"), .useRoutedTransfers_ = true, .withDistance_ = true}); auto const& sd = durations.street_durations_.value(); auto const& td = durations.transit_durations_.value(); ASSERT_EQ(4U, sd.size()); EXPECT_DOUBLE_EQ(344.0, sd.at(0).duration_.value()); EXPECT_NEAR(351.9, sd.at(0).distance_.value(), 0.1); EXPECT_DOUBLE_EQ(556.0, sd.at(1).duration_.value()); EXPECT_NEAR(607.0, sd.at(1).distance_.value(), 0.1); EXPECT_DOUBLE_EQ(966.0, sd.at(2).duration_.value()); EXPECT_NEAR(1100.6, sd.at(2).distance_.value(), 0.1); EXPECT_EQ(api::Duration{}, sd.at(3)); ASSERT_EQ(4U, td.size()); ASSERT_EQ(1U, td.at(0).size()); EXPECT_DOUBLE_EQ(1320.0, td.at(0).at(0).duration_); EXPECT_EQ(0, td.at(0).at(0).transfers_); ASSERT_EQ(1U, td.at(1).size()); EXPECT_DOUBLE_EQ(1680.0, td.at(1).at(0).duration_); EXPECT_EQ(0, td.at(1).at(0).transfers_); ASSERT_EQ(1U, td.at(2).size()); EXPECT_DOUBLE_EQ(1740.0, td.at(2).at(0).duration_); EXPECT_EQ(0, td.at(2).at(0).transfers_); ASSERT_EQ(1U, td.at(3).size()); EXPECT_DOUBLE_EQ(4440.0, td.at(3).at(0).duration_); EXPECT_EQ(2, td.at(3).at(0).transfers_); } TEST(one_to_many, pareto_sets_with_multiple_entries) { // Long walking paths + fast connctions => multiple durations // Currently: Long transfer times, so that transit is faster // After bug fix: Slow walking speed, so that transit is faster // might require moving stops (B2->T1, T1->T2, T2 delete) with paths: // Bus1 -> Tram3, Bus1 -> Bus2 -> Tram3, Bus1 -> Bus2 -> Tram1/2 -> Tram3 auto [d, _config] = get_test_case(); auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{ .one_ = "49.8722160,8.6282315", // DA_Bus_1 .many_ = {"49.8755778,8.6240518", // DA_Bus_2 "49.8752926,8.6277460", // DA_Tram_1 "49.871561,8.6320181"}, // DA_Tram_3 .time_ = parse_time("2019-05-01T00:05:00.000+02:00"), .maxPreTransitTime_ = 300}); // Prevent any pre transit to Tram_x // We only care about duration to DA_Tram_3, everything else is for debugging auto const& sd = durations.street_durations_.value(); ASSERT_EQ(3U, sd.size()); EXPECT_DOUBLE_EQ(966.0, sd.at(2).duration_.value()); EXPECT_EQ((std::optional>>{{ {{.duration_ = 1080.0, .transfers_ = 0}}, {{.duration_ = 1140.0, .transfers_ = 0}}, {{.duration_ = 1500.0, .transfers_ = 0}, {.duration_ = 1440.0, .transfers_ = 1}}, }}), durations.transit_durations_); } ================================================ FILE: test/endpoints/siri_sx_test.cc ================================================ #include "gtest/gtest.h" #include #include #include "date/date.h" #include "utl/init_from.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/trip.h" #include "motis/import.h" #include "motis/rt/auser.h" #include "motis/tag_lookup.h" using namespace motis; using namespace date; constexpr auto const kSiriSxGtfs = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone TEST,Test Agency,https://example.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code STOP1,Stop 1,48.0,9.0,0,, STOP2,Stop 2,48.1,9.1,0,, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type R1,TEST,R1,,,3 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id R1,S1,T1,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type T1,10:00:00,10:00:00,STOP1,1,0,0 T1,10:10:00,10:10:00,STOP2,2,0,0 # calendar_dates.txt service_id,date,exception_type S1,20260110,1 )"; constexpr auto const kSiriSxJsonUpdate = R"({ "responseTimestamp": "2026-01-10T09:55:00Z", "estimatedTimetableDelivery": { "estimatedJourneyVersionFrame": { "recordedAtTime": "2026-01-10T09:55:00Z", "estimatedVehicleJourney": { "lineRef": "R1", "directionRef": "0", "framedVehicleJourneyRef": { "dataFrameRef": "20260110", "datedVehicleJourneyRef": "40-1-24290-78300" }, "estimatedCalls": { "estimatedCall": [ { "stopPointRef": { "value": "STOP1" }, "order": 1, "extraCall": false, "cancellation": false, "aimedArrivalTime": "2026-01-10T10:00:00+01:00", "aimedDepartureTime": "2026-01-10T10:00:00+01:00" }, { "stopPointRef": { "value": "STOP2" }, "order": 2, "extraCall": false, "cancellation": false, "aimedArrivalTime": "2026-01-10T10:10:00+01:00", "aimedDepartureTime": "2026-01-10T10:10:00+01:00" } ] } } } }, "situationExchangeDelivery": { "situations": { "ptSituationElement": [ { "creationTime": { "value": "2026-01-10T09:00:00Z" }, "situationNumber": { "value": "S1" }, "validityPeriod": [ { "startTime": { "value": "2026-01-10T09:00:00Z" }, "endTime": { "value": "2026-01-10T12:00:00Z" } } ], "publicationWindow": { "startTime": { "value": "2026-01-10T09:00:00Z" } }, "severity": { "value": "noImpact" }, "affects": { "stopPlaces": { "affectedStopPlace": [ { "stopPlaceRef": { "value": "STOP1" } } ] }, "affectedLines": { "affectedLine": [ { "lineRef": { "value": "4-121-4" } }, { "lineRef": { "value": "4-104-4" } } ] } }, "summary": [ { "value": "Platform change" } ], "description": [ { "value": "Use platform 2" } ] }, { "creationTime": { "value": "2026-01-10T09:05:00Z" }, "situationNumber": { "value": "S2" }, "validityPeriod": [ { "startTime": { "value": "2026-01-10T09:00:00Z" }, "endTime": { "value": "2026-01-10T12:00:00Z" } } ], "publicationWindow": { "startTime": { "value": "2026-01-10T09:00:00Z" } }, "severity": { "value": "minor" }, "affects": { "networks": { "affectedNetwork": [ { "affectedLine": [ { "lineRef": { "value": "R1" } } ] } ] } }, "summary": [ { "value": "Line disruption" } ], "description": [ { "value": "R1 diverted due to works" } ] }, { "creationTime": { "value": "2026-01-10T09:10:00Z" }, "situationNumber": { "value": "S3" }, "validityPeriod": [ { "startTime": { "value": "2026-01-10T09:00:00Z" }, "endTime": { "value": "2026-01-10T12:00:00Z" } } ], "publicationWindow": { "startTime": { "value": "2026-01-10T09:00:00Z" } }, "severity": { "value": "minor" }, "affects": { "vehicleJourneys": { "affectedVehicleJourney": [ { "framedVehicleJourneyRef": { "dataFrameRef": { "value": "20260110" }, "datedVehicleJourneyRef": { "value": "40-1-24290-78300" } } } ] } }, "summary": [ { "value": "Vehicle journey issue" } ], "description": [ { "value": "Specific trip impacted" } ] } ] } } })"; TEST(motis, trip_siri_sx_alerts) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{ .timetable_ = config::timetable{.first_day_ = "2026-01-10", .num_days_ = 1, .datasets_ = {{"test", {.path_ = kSiriSxGtfs}}}}, .street_routing_ = false}; import(c, "test/data"); auto d = data{"test/data", c}; d.init_rtt(sys_days{2026_y / January / 10}); auto& rtt = *d.rt_->rtt_; auto siri_updater = auser(*d.tt_, d.tags_->get_src("test"), nigiri::rt::vdv_aus::updater::xml_format::kSiriJson); siri_updater.consume_update(kSiriSxJsonUpdate, rtt); auto const trip_ep = utl::init_from(d).value(); auto const res = trip_ep("?tripId=20260110_10%3A00_test_T1"); ASSERT_EQ(1, res.legs_.size()); auto const& leg = res.legs_.front(); ASSERT_TRUE(leg.from_.alerts_.has_value()); ASSERT_FALSE(leg.from_.alerts_->empty()); EXPECT_EQ("Platform change", leg.from_.alerts_->front().headerText_); EXPECT_EQ("Use platform 2", leg.from_.alerts_->front().descriptionText_); ASSERT_TRUE(leg.alerts_.has_value()); auto const has_line_alert = std::any_of( begin(*leg.alerts_), end(*leg.alerts_), [](api::Alert const& alert) { return alert.headerText_ == "Line disruption" && alert.descriptionText_ == "R1 diverted due to works"; }); EXPECT_TRUE(has_line_alert); auto const has_vehicle_alert = std::any_of( begin(*leg.alerts_), end(*leg.alerts_), [](api::Alert const& alert) { return alert.headerText_ == "Vehicle journey issue" && alert.descriptionText_ == "Specific trip impacted"; }); EXPECT_TRUE(has_vehicle_alert); } ================================================ FILE: test/endpoints/stop_group_geocoding_test.cc ================================================ #include "gtest/gtest.h" #include #include #include "utl/init_from.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/adr/geocode.h" #include "motis/import.h" using namespace motis; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station G1,Group 1,,0.0,0.0,,0, G2,Group 2,,0.0,0.0,,0, A,Stop A,,48.1,11.5,,0, B,Stop B,,48.2,11.6,,0, C,Stop C,,48.3,11.7,,0, D,Stop D,,48.4,11.8,,0, # stop_group_elements.txt stop_group_id,stop_id G1,A G2,B # calendar_dates.txt service_id,date,exception_type S1,20200101,1 # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type RB,DB,RB,,,3 RT,DB,RT,,,0 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id RB,S1,TB,RB, RT,S1,TT,RT, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type TB,10:00:00,10:00:00,A,1,0,0 TB,10:30:00,10:30:00,C,2,0,0 TT,11:00:00,11:00:00,B,1,0,0 TT,11:30:00,11:30:00,D,2,0,0 )"; TEST(motis, stop_group_geocoding) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{.timetable_ = config::timetable{.first_day_ = "2020-01-01", .num_days_ = 2, .datasets_ = {{"test", {.path_ = kGTFS}}}}, .geocoding_ = true}; import(c, "test/data"); auto d = data{"test/data", c}; auto const geocode = utl::init_from(d).value(); auto const g1 = geocode("/api/v1/geocode?text=Group%201"); auto const g1_it = utl::find_if(g1, [](auto const& m) { return m.id_ == "test_G1"; }); ASSERT_NE(end(g1), g1_it); ASSERT_TRUE(g1_it->modes_.has_value()); EXPECT_NE(end(*g1_it->modes_), utl::find(*g1_it->modes_, api::ModeEnum::BUS)); ASSERT_TRUE(g1_it->importance_.has_value()); EXPECT_GT(*g1_it->importance_, 0.0); auto const g2 = geocode("/api/v1/geocode?text=Group%202"); auto const g2_it = utl::find_if(g2, [](auto const& m) { return m.id_ == "test_G2"; }); ASSERT_NE(end(g2), g2_it); ASSERT_TRUE(g2_it->modes_.has_value()); EXPECT_NE(end(*g2_it->modes_), utl::find(*g2_it->modes_, api::ModeEnum::TRAM)); ASSERT_TRUE(g2_it->importance_.has_value()); EXPECT_GT(*g2_it->importance_, 0.0); } ================================================ FILE: test/endpoints/stop_times_test.cc ================================================ #include "motis/endpoints/stop_times.h" #include "gtest/gtest.h" #include #include #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/json.hpp" #include "net/bad_request_exception.h" #include "net/not_found_exception.h" #ifdef NO_DATA #undef NO_DATA #endif #include "gtfsrt/gtfs-realtime.pb.h" #include "utl/init_from.h" #include "nigiri/rt/gtfsrt_update.h" #include "motis-api/motis-api.h" #include "motis/config.h" #include "motis/data.h" #include "motis/elevators/elevators.h" #include "motis/elevators/parse_fasta.h" #include "motis/endpoints/routing.h" #include "motis/gbfs/update.h" #include "motis/import.h" #include "../util.h" namespace json = boost::json; using namespace std::string_view_literals; using namespace motis; using namespace date; using namespace std::chrono_literals; using namespace test; namespace n = nigiri; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3,DB,S3,,,109 U4,DB,U4,,,402 ICE,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3,S1,S3,,block_1 U4,S1,U4,,block_1 ICE,S1,ICE,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type U4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 U4,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:20:00,01:20:00,FFM_10,2,0,0 ICE,00:35:00,00:35:00,DA_10,0,0,0 ICE,00:45:00,00:45:00,FFM_10,1,0,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 )"; TEST(motis, stop_times) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{.timetable_ = config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .datasets_ = {{"test", {.path_ = kGTFS}}}}}; import(c, "test/data"); auto d = data{"test/data", c}; d.init_rtt(date::sys_days{2019_y / May / 1}); auto const stats = n::rt::gtfsrt_update_msg( *d.tt_, *d.rt_->rtt_, n::source_idx_t{0}, "test", to_feed_msg({trip_update{ .trip_ = {.trip_id_ = "ICE", .start_time_ = {"00:35:00"}, .date_ = {"20190501"}}, .stop_updates_ = {{.stop_id_ = "FFM_12", .seq_ = std::optional{1U}, .ev_type_ = n::event_type::kArr, .delay_minutes_ = 10, .stop_assignment_ = "FFM_12"}}}, alert{ .header_ = "Yeah", .description_ = "Yeah!!", .entities_ = {{.trip_ = { {.trip_id_ = "ICE", .start_time_ = {"00:35:00"}, .date_ = {"20190501"}}, }, .stop_id_ = "DA"}}}, alert{.header_ = "Hello", .description_ = "World", .entities_ = {{.trip_ = {{.trip_id_ = "ICE", .start_time_ = {"00:35:00"}, .date_ = {"20190501"}}}}}}}, date::sys_days{2019_y / May / 1} + 9h)); EXPECT_EQ(1U, stats.total_entities_success_); EXPECT_EQ(2U, stats.alert_total_resolve_success_); auto const stop_times = utl::init_from(d).value(); EXPECT_EQ(d.rt_->rtt_.get(), stop_times.rt_->rtt_.get()); { auto const res = stop_times( "/api/v5/stoptimes?stopId=test_FFM_10" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3" "&language=de" "&fetchStops=true"); auto const format_time = [&](auto&& t, char const* fmt = "%F %H:%M") { return date::format(fmt, *t); }; EXPECT_EQ("test_FFM_10", res.place_.stopId_); EXPECT_EQ(3, res.stopTimes_.size()); auto const& ice = res.stopTimes_[0]; EXPECT_EQ(api::ModeEnum::HIGHSPEED_RAIL, ice.mode_); EXPECT_EQ("20190501_00:35_test_ICE", ice.tripId_); EXPECT_EQ("test_DA_10", ice.tripFrom_.stopId_); EXPECT_EQ("test_FFM_12", ice.tripTo_.stopId_); EXPECT_EQ("ICE", ice.displayName_); EXPECT_EQ("FFM Hbf", ice.headsign_); EXPECT_EQ("test_ICE", ice.routeId_); EXPECT_EQ("2019-04-30 22:55", format_time(ice.place_.arrival_.value())); EXPECT_EQ("2019-04-30 22:45", format_time(ice.place_.scheduledArrival_.value())); EXPECT_EQ(true, ice.realTime_); EXPECT_EQ(1, ice.previousStops_->size()); EXPECT_EQ(1, ice.place_.alerts_->size()); auto const& sbahn = res.stopTimes_[2]; EXPECT_EQ( api::ModeEnum::SUBWAY, sbahn.mode_); // mode can't change with block_id so sticks from U4 EXPECT_EQ("20190501_01:15_test_S3", sbahn.tripId_); EXPECT_EQ("test_FFM_101", sbahn.tripFrom_.stopId_); EXPECT_EQ("test_FFM_10", sbahn.tripTo_.stopId_); EXPECT_EQ("S3", sbahn.displayName_); EXPECT_EQ("FFM Hbf", sbahn.headsign_); EXPECT_EQ("test_S3", sbahn.routeId_); EXPECT_EQ("2019-04-30 23:20", format_time(sbahn.place_.arrival_.value())); EXPECT_EQ("2019-04-30 23:20", format_time(sbahn.place_.scheduledArrival_.value())); EXPECT_EQ(false, sbahn.realTime_); EXPECT_EQ(2, sbahn.previousStops_->size()); } { // same test with alerts off auto const res2 = stop_times( "/api/v5/stoptimes?stopId=test_FFM_10" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3" "&language=de" "&fetchStops=true" "&withAlerts=false"); EXPECT_EQ(3, res2.stopTimes_.size()); for (auto const& stopTime : res2.stopTimes_) { EXPECT_FALSE(stopTime.place_.alerts_.has_value()); } } { // center-only query, radius is required auto const res = stop_times( "/api/v5/stoptimes?center=50.10593,8.66118" "&radius=250" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3" "&language=de" "&fetchStops=true"); EXPECT_EQ("center", res.place_.name_); EXPECT_FALSE(res.place_.stopId_.has_value()); EXPECT_FALSE(res.stopTimes_.empty()); } { // invalid stopId without center EXPECT_THROW(stop_times("/api/v5/stoptimes?stopId=test_SOMETHING_RANDOM" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3"), net::not_found_exception); } { // invalid stopId should fall back to center auto const res = stop_times( "/api/v5/stoptimes?stopId=test_SOMETHING_RANDOM" "¢er=50.10593,8.66118" "&radius=250" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3" "&language=de"); EXPECT_EQ("center", res.place_.name_); EXPECT_FALSE(res.place_.stopId_.has_value()); EXPECT_FALSE(res.stopTimes_.empty()); } { // stoptimes in radius = r auto const r = 110.0; auto const center = geo::latlng{50.10563, 8.66218}; auto const res = stop_times(std::format("/api/v5/stoptimes?center={},{}" "&radius={}" "&exactRadius=true" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=200" "&language=de" "&fetchStops=true", center.lat_, center.lng_, r)); EXPECT_FALSE(res.stopTimes_.empty()); for (auto const& v : res.stopTimes_) { auto const dist = geo::distance(center, geo::latlng{v.place_.lat_, v.place_.lon_}); EXPECT_LE(dist, r); } } { // neither stopId nor center -> panic EXPECT_THROW(stop_times("/api/v5/stoptimes?time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3"), net::bad_request_exception); } { // center without stopId requires radius EXPECT_THROW(stop_times("/api/v5/stoptimes?center=50.10593,8.66118" "&time=2019-04-30T23:30:00.000Z" "&arriveBy=true" "&n=3"), net::bad_request_exception); } { // window query LATER auto const res = stop_times( "/api/v5/stoptimes?stopId=test_FFM_101" "&time=2019-04-30T23:00:00.000Z" "&arriveBy=true" "&direction=LATER" "&window=1800" "&language=de"); auto const format_time = [&](auto&& t, char const* fmt = "%F %H:%M") { return date::format(fmt, *t); }; EXPECT_EQ(2, res.stopTimes_.size()); // n is ignored if window is set for (auto const& stop_time : res.stopTimes_) { auto const arr = format_time(stop_time.place_.arrival_.value()); std::cout << "arr: " << arr << std::endl; EXPECT_GE(arr, "2019-04-30 23:00"); EXPECT_LE(arr, "2019-04-30 23:30"); } EXPECT_FALSE(res.previousPageCursor_.empty()); EXPECT_FALSE(res.nextPageCursor_.empty()); } { // window query EARLIER auto const res = stop_times( "/api/v5/stoptimes?stopId=test_FFM_101" "&time=2019-04-30T23:15:00.000Z" "&arriveBy=true" "&direction=EARLIER" "&window=1800" "&language=de"); auto const format_time = [&](auto&& t, char const* fmt = "%F %H:%M") { return date::format(fmt, *t); }; for (auto const& stop_time : res.stopTimes_) { auto const arr = format_time(stop_time.place_.arrival_.value()); std::cout << "arr E: " << arr << std::endl; EXPECT_GE(arr, "2019-04-30 22:45"); EXPECT_LE(arr, "2019-04-30 23:15"); } } { // window query EARLIER (small window large n) auto const res = stop_times( "/api/v5/stoptimes?stopId=test_FFM_101" "&time=2019-04-30T23:15:00.000Z" "&arriveBy=true" "&direction=LATER" "&window=60" "&n=2" "&language=de"); auto const format_time = [&](auto&& t, char const* fmt = "%F %H:%M") { return date::format(fmt, *t); }; EXPECT_GT(res.stopTimes_.size(), 1); for (auto const& stop_time : res.stopTimes_) { auto const arr = format_time(stop_time.place_.arrival_.value()); std::cout << "arr E2: " << arr << std::endl; } } } ================================================ FILE: test/endpoints/trip_test.cc ================================================ #include "gtest/gtest.h" #include "utl/init_from.h" #include "nigiri/common/parse_time.h" #include "nigiri/rt/create_rt_timetable.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/rt_timetable.h" #include "motis/config.h" #include "motis/data.h" #include "motis/endpoints/trip.h" #include "motis/import.h" #include "motis/rt/auser.h" #include "motis/tag_lookup.h" using namespace std::string_view_literals; using namespace motis; using namespace date; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code Parent1,Parent1,50.0,8.0,1,, Child1A,Child1A,50.001,8.001,0,Parent1,1 Child1B,Child1B,50.002,8.002,0,Parent1,2 Parent2,Parent2,51.0,9.0,1,, Child2,Child2,51.001,9.001,0,Parent2,1 # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type R1,DB,R1,R1,,109 R2,DB,R2,R2,,109 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id R1,S1,T1,Parent2 Express, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type,stop_headsign T1,10:00:00,10:00:00,Child1A,1,0,0,Origin T1,10:10:00,10:10:00,Child1B,2,0,0,Midway T1,11:00:00,11:00:00,Child2,3,0,0,Destination # calendar_dates.txt service_id,date,exception_type S1,20190501,1 # translations.txt table_name,field_name,language,translation,record_id,record_sub_id,field_value routes,route_long_name,de,DE-R1,,,R1 routes,route_long_name,fr,FR-R1,,,R1 routes,route_long_name,en,EN-R1,,,R1 stops,stop_name,en,Child1A,Child1A,, stops,stop_name,de,Kind 1A,Child1A,, stops,stop_name,en,Child1B,,,Child1B stops,stop_name,de,Kind 1B,,,Child1B stops,stop_name,en,Parent2,Parent2,, stops,stop_name,de,Eltern 2,Parent2,, stops,stop_name,fr,Parent Deux,Parent2,, stops,stop_name,fr,Enfant 1A,Child1A,, stops,stop_name,fr,Enfant 1B,,,Child1B stop_times,stop_headsign,en,Parent2 Express,T1,1, stop_times,stop_headsign,de,Richtung Eltern Zwei,T1,1, stop_times,stop_headsign,fr,Vers Parent Deux,T1,1, )"; constexpr auto kScript = R"( function process_route(route) route:set_short_name({ translation.new('en', 'EN_SHORT_NAME'), translation.new('de', 'DE_SHORT_NAME'), translation.new('fr', 'FR_SHORT_NAME') }) route:get_short_name_translations():add(translation.new('hu', 'HU_SHORT_NAME')) print(route:get_short_name_translations():get(1):get_text()) print(route:get_short_name_translations():get(1):get_language()) end )"; TEST(motis, trip_stop_naming) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{ .timetable_ = config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .datasets_ = {{"test", {.path_ = kGTFS, .script_ = kScript}}}}, .street_routing_ = false}; import(c, "test/data"); auto d = data{"test/data", c}; auto const trip_ep = utl::init_from(d).value(); auto const res = trip_ep("?tripId=20190501_10%3A00_test_T1"); ASSERT_EQ(1, res.legs_.size()); auto const& leg = res.legs_[0]; EXPECT_GT(leg.legGeometry_.length_, 0); EXPECT_EQ("Child1A", leg.from_.name_); ASSERT_TRUE(leg.intermediateStops_.has_value()); ASSERT_EQ(1, leg.intermediateStops_->size()); EXPECT_EQ("Child1B", leg.intermediateStops_->at(0).name_); EXPECT_EQ("Parent2", leg.to_.name_); EXPECT_EQ("Parent2 Express", leg.headsign_); EXPECT_EQ("EN_SHORT_NAME", leg.routeShortName_); EXPECT_EQ("EN-R1", leg.routeLongName_); auto const compact_res = trip_ep("?tripId=20190501_10%3A00_test_T1&detailedLegs=false"); ASSERT_EQ(1, compact_res.legs_.size()); EXPECT_EQ("", compact_res.legs_[0].legGeometry_.points_); EXPECT_EQ(0, compact_res.legs_[0].legGeometry_.length_); auto const res_de = trip_ep("?tripId=20190501_10%3A00_test_T1&language=de"); ASSERT_EQ(1, res_de.legs_.size()); auto const& leg_de = res_de.legs_[0]; EXPECT_EQ("Kind 1A", leg_de.from_.name_); ASSERT_TRUE(leg_de.intermediateStops_.has_value()); ASSERT_EQ(1, leg_de.intermediateStops_->size()); EXPECT_EQ("Kind 1B", leg_de.intermediateStops_->at(0).name_); EXPECT_EQ("Eltern 2", leg_de.to_.name_); EXPECT_EQ("Richtung Eltern Zwei", leg_de.headsign_); EXPECT_EQ("DE_SHORT_NAME", leg_de.routeShortName_); EXPECT_EQ("DE-R1", leg_de.routeLongName_); auto const res_fr = trip_ep("?tripId=20190501_10%3A00_test_T1&language=fr"); ASSERT_EQ(1, res_fr.legs_.size()); auto const& leg_fr = res_fr.legs_[0]; EXPECT_EQ("Enfant 1A", leg_fr.from_.name_); ASSERT_TRUE(leg_fr.intermediateStops_.has_value()); ASSERT_EQ(1, leg_fr.intermediateStops_->size()); EXPECT_EQ("Enfant 1B", leg_fr.intermediateStops_->at(0).name_); EXPECT_EQ("Parent Deux", leg_fr.to_.name_); EXPECT_EQ("Vers Parent Deux", leg_fr.headsign_); EXPECT_EQ("FR_SHORT_NAME", leg_fr.routeShortName_); EXPECT_EQ("FR-R1", leg_fr.routeLongName_); } constexpr auto kNetex = R"( # netex.xml 2025-06-26T14:16:54+02:00 INTERMAPS 1 2 de 2024-12-15T00:00:00 2025-12-14T23:59:59 ProductCategories Pendelbahn PB Cabin CBN PB Test Operator 2024-12-15T00:00:00 2024-12-15T23:59:59 1 SLOID ch:1:sloid:30243 Bettmeralp Talstation (Seilb.) 8.1967 46.3803 SLOID ch:1:sloid:30243:0:403158 Bettmeralp Talstation (Seilb.) 8.1967 46.3803 SLOID ch:1:sloid:1954 Bettmeralp 8.1977 46.4219 SLOID ch:1:sloid:1954:0:845083 Bettmeralp 8.1977 46.4219 2336 - Betten Talstation - Bettmeralp (Direkt) bus localBus Bettmeralp Bettmeralp Talstation (Seilb.) Bettmeralp Free Internet with the SBB FreeSurf app Connexion Internet gratuite avec l'app FreeSurf CFF Connessione Internet gratuita con l'app FreeSurf FFS Gratis-Internet mit der App SBB FreeSurf A__FS A__FS true TripNr 2336 05:50:00 PT7M inbound )"; TEST(motis, trip_notice_translations) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{.timetable_ = config::timetable{.first_day_ = "2024-12-15", .num_days_ = 2, .datasets_ = {{"netex", {.path_ = kNetex}}}}, .street_routing_ = false}; import(c, "test/data"); auto d = data{"test/data", c}; auto const day = date::sys_days{2024_y / December / 15}; d.init_rtt(day); auto& rtt = *d.rt_->rtt_; auto const trip_ep = utl::init_from(d).value(); auto const base_trip = std::string{ "?tripId=20241215_05%3A50_netex_ch%3A1%3AServiceJourney%3Awhatever"}; auto const expect_notice = [&](std::optional language, std::string_view expected) { auto url = base_trip; if (language.has_value()) { url += "&language="; url.append(language->data(), language->size()); } auto const res = trip_ep(url); ASSERT_EQ(1, res.legs_.size()); auto const& leg = res.legs_[0]; ASSERT_TRUE(leg.alerts_.has_value()); ASSERT_FALSE(leg.alerts_->empty()); EXPECT_EQ(expected, leg.alerts_->front().headerText_); }; expect_notice(std::nullopt, "Gratis-Internet mit der App SBB FreeSurf"); expect_notice("de"sv, "Gratis-Internet mit der App SBB FreeSurf"); expect_notice("en"sv, "Free Internet with the SBB FreeSurf app"); expect_notice("fr"sv, "Connexion Internet gratuite avec l'app FreeSurf CFF"); expect_notice("it"sv, "Connessione Internet gratuita con l'app FreeSurf FFS"); auto const sched_dep = std::chrono::time_point_cast( nigiri::parse_time("2024-12-15T05:50:00+01:00", "%FT%T%Ez")); auto const sched_arr = std::chrono::time_point_cast( nigiri::parse_time("2024-12-15T05:57:00+01:00", "%FT%T%Ez")); auto const check_leg = [&](api::Leg const& leg, std::chrono::sys_seconds const dep, std::chrono::sys_seconds const arr, bool const is_rt) { EXPECT_EQ(dep, *leg.startTime_); EXPECT_EQ(arr, *leg.endTime_); EXPECT_EQ(sched_dep, *leg.scheduledStartTime_); EXPECT_EQ(sched_arr, *leg.scheduledEndTime_); EXPECT_EQ(is_rt, leg.realTime_); }; auto const base_res = trip_ep(base_trip); ASSERT_EQ(1, base_res.legs_.size()); check_leg(base_res.legs_.front(), sched_dep, sched_arr, false); { constexpr auto kNetexSiriUpdate = R"( 2024-12-15T05:40:00 2024-12-15T05:40:00 2024-12-15T05:40:00 LineDoesNotMatter Up 2024-12-15 unknown ch:1:sloid:30243:0:403158 2024-12-15T05:50:00+01:00 2024-12-15T05:52:00+01:00 ch:1:sloid:1954:0:845083 2024-12-15T05:57:00+01:00 2024-12-15T05:59:00+01:00 )"; auto siri_updater = auser(*d.tt_, d.tags_->get_src("netex"), nigiri::rt::vdv_aus::updater::xml_format::kSiri); auto const expected_siri_state = nigiri::parse_time_no_tz("2024-12-15T05:40:00") .time_since_epoch() .count(); auto const siri_stats = siri_updater.consume_update(kNetexSiriUpdate, rtt); EXPECT_EQ(1U, siri_stats.matched_runs_); EXPECT_EQ(2U, siri_stats.updated_events_); EXPECT_EQ(expected_siri_state, siri_updater.update_state_); auto const siri_dep = std::chrono::time_point_cast( nigiri::parse_time("2024-12-15T05:52:00+01:00", "%FT%T%Ez")); auto const siri_arr = std::chrono::time_point_cast( nigiri::parse_time("2024-12-15T05:59:00+01:00", "%FT%T%Ez")); auto const siri_res = trip_ep(base_trip); ASSERT_EQ(1, siri_res.legs_.size()); check_leg(siri_res.legs_.front(), siri_dep, siri_arr, true); } { constexpr auto kNetexVdvUpdate = R"( NET 1 NETEX 2024-12-15 true MOTIS ch:1:sloid:30243:0:403158 2024-12-15T04:50:00 2024-12-15T04:56:00 ch:1:sloid:1954:0:845083 2024-12-15T04:57:00 2024-12-15T05:03:00 NET NET Netex Demo false false )"; auto vdv_updater = auser(*d.tt_, d.tags_->get_src("netex"), nigiri::rt::vdv_aus::updater::xml_format::kVdv); auto const vdv_stats = vdv_updater.consume_update(kNetexVdvUpdate, rtt); EXPECT_EQ(1U, vdv_stats.matched_runs_); EXPECT_EQ(2U, vdv_stats.updated_events_); EXPECT_EQ(314159, vdv_updater.update_state_); auto const vdv_dep = std::chrono::time_point_cast( nigiri::parse_time("2024-12-15T04:56:00", "%FT%T")); auto const vdv_arr = std::chrono::time_point_cast( nigiri::parse_time("2024-12-15T05:03:00", "%FT%T")); auto const vdv_res = trip_ep(base_trip); ASSERT_EQ(1, vdv_res.legs_.size()); check_leg(vdv_res.legs_.front(), vdv_dep, vdv_arr, true); } } ================================================ FILE: test/ffm_hbf.osm ================================================ [File too large to display: 15.9 MB] ================================================ FILE: test/flex_mode_id_test.cc ================================================ #include "gtest/gtest.h" #include "motis/flex/mode_id.h" using namespace motis::flex; TEST(motis, flex_mode_id_zero) { auto const t = nigiri::flex_transport_idx_t{0U}; auto const stop = 0U; auto const dir = osr::direction::kForward; auto const id = mode_id{t, stop, dir}.to_id(); EXPECT_TRUE(mode_id::is_flex(id)); auto const id1 = mode_id{id}; EXPECT_EQ(stop, id1.get_stop()); EXPECT_EQ(dir, id1.get_dir()); EXPECT_EQ(t, id1.get_flex_transport()); } TEST(motis, flex_mode_id) { auto const t = nigiri::flex_transport_idx_t{44444U}; auto const stop = 15; auto const dir = osr::direction::kBackward; auto const id = mode_id{t, stop, dir}.to_id(); EXPECT_TRUE(mode_id::is_flex(id)); auto const id1 = mode_id{id}; EXPECT_EQ(stop, id1.get_stop()); EXPECT_EQ(dir, id1.get_dir()); EXPECT_EQ(t, id1.get_flex_transport()); } ================================================ FILE: test/gbfs_partition_test.cc ================================================ #include "gtest/gtest.h" #include #include #include #include #include "motis/gbfs/partition.h" using namespace motis::gbfs; // helper function to compare sets regardless of order template bool compare_partitions(std::vector> const& actual, std::vector> const& expected) { if (actual.size() != expected.size()) { return false; } auto actual_sets = std::vector>{}; auto expected_sets = std::vector>{}; for (auto const& vec : actual) { actual_sets.emplace_back(begin(vec), end(vec)); } for (auto const& vec : expected) { expected_sets.emplace_back(begin(vec), end(vec)); } std::sort(actual_sets.begin(), actual_sets.end()); std::sort(expected_sets.begin(), expected_sets.end()); return actual_sets == expected_sets; } TEST(motis, gbfs_partition_test) { using T = int; auto p = partition{10}; // Initial partition test auto const single_set = std::vector>{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}; EXPECT_TRUE(compare_partitions(p.get_sets(), single_set)); // First refinement p.refine(std::array{1, 2, 3}); auto const two_sets = std::vector>{{1, 2, 3}, {0, 4, 5, 6, 7, 8, 9}}; EXPECT_TRUE(compare_partitions(p.get_sets(), two_sets)); // Same refinement in different orders should yield same result p.refine(std::array{1, 2, 3}); EXPECT_TRUE(compare_partitions(p.get_sets(), two_sets)); p.refine(std::array{3, 2, 1}); EXPECT_TRUE(compare_partitions(p.get_sets(), two_sets)); // Further refinement p.refine(std::array{7, 8}); auto const three_sets = std::vector>{{1, 2, 3}, {7, 8}, {0, 4, 5, 6, 9}}; EXPECT_TRUE(compare_partitions(p.get_sets(), three_sets)); // Final refinement p.refine(std::array{1, 3}); auto const four_sets = std::vector>{{1, 3}, {2}, {7, 8}, {0, 4, 5, 6, 9}}; EXPECT_TRUE(compare_partitions(p.get_sets(), four_sets)); } TEST(motis, gbfs_partition_empty_refinement) { using T = int; auto p = partition{5}; p.refine(std::array{}); EXPECT_TRUE(compare_partitions(p.get_sets(), std::vector>{{0, 1, 2, 3, 4}})); } TEST(motis, gbfs_partition_single_element) { using T = int; auto p = partition{1}; p.refine(std::array{0}); EXPECT_TRUE( compare_partitions(p.get_sets(), std::vector>{{0}})); } TEST(motis, gbfs_partition_disjoint_refinements) { using T = int; auto p = partition{6}; p.refine(std::array{0, 1}); p.refine(std::array{2, 3}); p.refine(std::array{4, 5}); auto const expected = std::vector>{{0, 1}, {2, 3}, {4, 5}}; EXPECT_TRUE(compare_partitions(p.get_sets(), expected)); } ================================================ FILE: test/main.cc ================================================ #include #include "google/protobuf/arena.h" #include "gtest/gtest.h" #include "utl/progress_tracker.h" #include "test_dir.h" #ifdef PROTOBUF_LINKED #include "google/protobuf/stubs/common.h" #endif namespace fs = std::filesystem; int main(int argc, char** argv) { std::clog.rdbuf(std::cout.rdbuf()); auto const progress_tracker = utl::activate_progress_tracker("test"); auto const silencer = utl::global_progress_bars{true}; fs::current_path(OSR_TEST_EXECUTION_DIR); ::testing::InitGoogleTest(&argc, argv); auto test_result = RUN_ALL_TESTS(); google::protobuf::ShutdownProtobufLibrary(); return test_result; } ================================================ FILE: test/matching_test.cc ================================================ #include "gtest/gtest.h" #include "osr/platforms.h" #include "osr/routing/profile.h" #include "osr/routing/profiles/bike.h" #include "osr/routing/profiles/bike_sharing.h" #include "osr/routing/profiles/car.h" #include "osr/routing/profiles/car_sharing.h" #include "osr/routing/profiles/foot.h" #include "osr/types.h" #include "nigiri/timetable.h" #include "nigiri/types.h" #include "motis/config.h" #include "motis/data.h" #include "motis/import.h" #include "motis/match_platforms.h" #include "motis/osr/parameters.h" using namespace std::string_view_literals; using namespace osr; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3,DB,S3,,,109 U4,DB,U4,,,402 ICE,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3,S1,S3,, U4,S1,U4,, ICE,S1,ICE,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type S3,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0 U4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 U4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0 ICE,00:35:00,00:35:00,DA_10,0,0,0 ICE,00:45:00,00:45:00,FFM_10,1,0,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 # frequencies.txt trip_id,start_time,end_time,headway_secs S3,01:15:00,25:15:00,3600 ICE,00:35:00,24:35:00,3600 U4,01:05:00,25:01:00,3600 )"sv; TEST(motis, get_track) { ASSERT_FALSE(motis::get_track("a:").has_value()); auto const track = motis::get_track("a:232"); ASSERT_TRUE(track.has_value()); EXPECT_EQ("232", *track); auto const track_1 = motis::get_track("232"); ASSERT_TRUE(track_1.has_value()); EXPECT_EQ("232", *track_1); } TEST(motis, get_way_candidates) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = motis::config{ .server_ = {{.web_folder_ = "ui/build", .n_threads_ = 1U}}, .osm_ = {"test/resources/test_case.osm.pbf"}, .timetable_ = motis::config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .use_osm_stop_coordinates_ = true, .extend_missing_footpaths_ = false, .preprocess_max_matching_distance_ = 250, .datasets_ = {{"test", {.path_ = std::string{kGTFS}}}}}, .street_routing_ = true, .osr_footpath_ = true, .geocoding_ = true, .reverse_geocoding_ = true}; motis::import(c, "test/data"); auto d = motis::data{"test/data", c}; auto const location_idxs = utl::to_vec(utl::enumerate(d.tt_->locations_.src_), [&](std::tuple const ll) { return nigiri::location_idx_t{std::get<0>(ll)}; }); auto const locs = utl::to_vec(location_idxs, [&](nigiri::location_idx_t const l) { return osr::location{ d.tt_->locations_.coordinates_[nigiri::location_idx_t{l}], d.pl_->get_level(*d.w_, (*d.matches_)[nigiri::location_idx_t{l}])}; }); auto const get_path = [&](search_profile const p, way_candidate const& a, node_candidate const& anc, location const& l) { switch (p) { case search_profile::kFoot: [[fallthrough]]; case search_profile::kWheelchair: [[fallthrough]]; case search_profile::kCar: [[fallthrough]]; case search_profile::kBike: [[fallthrough]]; case search_profile::kCarSharing: [[fallthrough]]; case search_profile::kBikeSharing: return d.l_->get_node_candidate_path(a, anc, true, l); default: return std::vector{}; } }; for (auto profile : {osr::search_profile::kCar, osr::search_profile::kCarSharing, osr::search_profile::kFoot, osr::search_profile::kWheelchair, osr::search_profile::kBike, osr::search_profile::kBikeSharing}) { auto const with_preprocessing = motis::get_reverse_platform_way_matches( *d.l_, &*d.way_matches_, profile, location_idxs, locs, osr::direction::kForward, 250); auto const without_preprocessing = utl::to_vec( utl::zip(location_idxs, locs), [&](std::tuple const ll) { auto const& [l, query] = ll; return d.l_->match(motis::to_profile_parameters(profile, {}), query, true, osr::direction::kForward, 250, nullptr, profile); }); ASSERT_EQ(with_preprocessing.size(), without_preprocessing.size()); for (auto const [with, without, l] : utl::zip(with_preprocessing, without_preprocessing, locs)) { ASSERT_EQ(with.size(), without.size()); auto sorted_with = with; auto sorted_without = without; auto const sort_by_way = [&](auto const& a, auto const& b) { return a.way_ < b.way_; }; utl::sort(sorted_with, sort_by_way); utl::sort(sorted_without, sort_by_way); for (auto [a, b] : utl::zip(sorted_with, sorted_without)) { ASSERT_FLOAT_EQ(a.dist_to_way_, b.dist_to_way_); ASSERT_EQ(a.way_, b.way_); for (auto const& [anc, bnc] : {std::tuple{a.left_, b.left_}, std::tuple{a.right_, b.right_}}) { ASSERT_EQ(anc.node_, bnc.node_); if (anc.valid()) { EXPECT_FLOAT_EQ(anc.dist_to_node_, bnc.dist_to_node_); EXPECT_EQ(anc.cost_, bnc.cost_); EXPECT_EQ(anc.lvl_, bnc.lvl_); EXPECT_EQ(anc.way_dir_, bnc.way_dir_); EXPECT_EQ(get_path(profile, a, anc, l), bnc.path_); } } } } auto const with_preprocessing_but_larger = motis::get_reverse_platform_way_matches(*d.l_, &*d.way_matches_, profile, location_idxs, locs, osr::direction::kForward, 500); auto const with_preprocessing_but_smaller = motis::get_reverse_platform_way_matches(*d.l_, &*d.way_matches_, profile, location_idxs, locs, osr::direction::kForward, 50); for (auto const [with, larger, smaller] : utl::zip(with_preprocessing, with_preprocessing_but_larger, with_preprocessing_but_smaller)) { if (with.size() == 0 && larger.size() == 0 && smaller.size() == 0) { continue; } EXPECT_GE(larger.size(), with.size()); EXPECT_GE(with.size(), smaller.size()); auto const& a = larger[0]; auto const& b = smaller[0]; EXPECT_TRUE(!a.left_.valid() || a.left_.path_.size() != 0); // on the fly match EXPECT_TRUE(!b.left_.valid() || b.left_.path_.size() == 0); // preprocessed match } for (auto dist : {5, 10, 25, 250, 1000}) { auto const remote_station = osr::location{{49.8731904, 8.6221451}, level_t{}}; auto const raw = d.l_->get_raw_match(remote_station, dist); auto const params = motis::to_profile_parameters(profile, {}); auto const with = d.l_->match(params, remote_station, true, osr::direction::kForward, dist, nullptr, profile, raw); auto const without = d.l_->match(params, remote_station, true, osr::direction::kForward, dist, nullptr, profile); EXPECT_NE(0, raw.size()); EXPECT_EQ(with.size(), without.size()); } } } ================================================ FILE: test/odm/csv_journey_test.cc ================================================ #include "gtest/gtest.h" #include "motis/odm/journeys.h" #include "motis/odm/odm.h" using namespace std::string_view_literals; using namespace date; using namespace std::chrono_literals; using namespace motis::odm; constexpr auto const csv0 = R"__(departure, arrival, transfers, first_mile_mode, first_mile_duration, last_mile_mode, last_mile_duration 2025-06-16 12:34, 2025-06-16 23:45, 3, taxi, 12, walk, 04 2025-06-17 11:11, 2025-06-17 22:22, 2, walk, 05, walk, 06 2025-06-18 06:33, 2025-06-18 07:44, 8, taxi, 20, taxi, 20 )__"sv; TEST(odm, csv_journeys_in_out) { EXPECT_EQ(csv0, to_csv(from_csv(csv0))); } constexpr auto const direct_odm_csv = R"__(departure, arrival, transfers, first_mile_mode, first_mile_duration, last_mile_mode, last_mile_duration 2025-06-16 12:00, 2025-06-16 13:00, 0, taxi, 60, walk, 00 )__"sv; TEST(odm, csv_journeys_direct) { EXPECT_EQ( direct_odm_csv, to_csv(std::vector{make_odm_direct( nigiri::location_idx_t::invalid(), nigiri::location_idx_t::invalid(), nigiri::unixtime_t{date::sys_days{2025_y / June / 16} + 12h}, nigiri::unixtime_t{date::sys_days{2025_y / June / 16} + 13h})})); } ================================================ FILE: test/odm/prima_test.cc ================================================ #include "gtest/gtest.h" #include "nigiri/loader/dir.h" #include "nigiri/loader/gtfs/load_timetable.h" #include "nigiri/loader/init_finish.h" #include "nigiri/common/parse_time.h" #include "nigiri/routing/journey.h" #include "nigiri/special_stations.h" #include "motis/odm/odm.h" #include "motis/odm/prima.h" #include "motis/transport_mode_ids.h" #include "motis-api/motis-api.h" namespace n = nigiri; namespace nr = nigiri::routing; using namespace motis::odm; using namespace std::chrono_literals; using namespace date; n::loader::mem_dir tt_files() { return n::loader::mem_dir::read(R"__( "( # stops.txt stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station A,A,A,0.1,0.1,,,, B,B,B,0.2,0.2,,,, C,C,C,0.3,0.3,,,, D,D,D,0.4,0.4,,,, )__"); } constexpr auto blacklist_request = R"({"start":{"lat":0E0,"lng":0E0},"target":{"lat":0E0,"lng":0E0},"startBusStops":[{"lat":1E-1,"lng":1E-1},{"lat":2E-1,"lng":2E-1}],"targetBusStops":[{"lat":3.0000000000000004E-1,"lng":3.0000000000000004E-1},{"lat":4E-1,"lng":4E-1}],"earliest":0,"latest":172800000,"startFixed":true,"capacities":{"wheelchairs":1,"bikes":0,"passengers":1,"luggage":0}})"; constexpr auto invalid_response = R"({"message":"Internal Error"})"; constexpr auto blacklist_response = R"( { "start": [[{"startTime": 32400000, "endTime": 43200000}],[{"startTime": 43200000, "endTime": 64800000}]], "target": [[{"startTime": 43200000, "endTime": 64800000}],[]], "direct": [{"startTime": 43200000, "endTime": 64800000}] } )"; // 1970-01-01T09:57:00Z, 1970-01-01T10:55:00Z // 1970-01-01T14:07:00Z, 1970-01-01T14:46:00Z // 1970-01-01T11:30:00Z, 1970-01-01T12:30:00Z constexpr auto whitelisting_response = R"( { "start": [[{"pickupTime": 35820000, "dropoffTime": 39300000}],[null]], "target": [[{"pickupTime": 50820000, "dropoffTime": 53160000}]], "direct": [{"pickupTime": 41400000,"dropoffTime": 45000000}] } )"; constexpr auto adjusted_to_whitelisting = R"( [1970-01-01 09:57, 1970-01-01 12:00] TRANSFERS: 0 FROM: (START, START) [1970-01-01 09:57] TO: (END, END) [1970-01-01 12:00] leg 0: (START, START) [1970-01-01 09:57] -> (A, A) [1970-01-01 10:55] MUMO (id=16, duration=58) leg 1: (A, A) [1970-01-01 10:55] -> (A, A) [1970-01-01 11:00] FOOTPATH (duration=5) leg 2: (A, A) [1970-01-01 11:00] -> (END, END) [1970-01-01 12:00] MUMO (id=0, duration=60) [1970-01-01 09:57, 1970-01-01 14:46] TRANSFERS: 0 FROM: (START, START) [1970-01-01 09:57] TO: (END, END) [1970-01-01 14:46] leg 0: (START, START) [1970-01-01 09:57] -> (A, A) [1970-01-01 10:55] MUMO (id=16, duration=58) leg 1: (A, A) [1970-01-01 10:55] -> (A, A) [1970-01-01 11:00] FOOTPATH (duration=5) leg 2: (A, A) [1970-01-01 11:00] -> (C, C) [1970-01-01 13:00] MUMO (id=1000000, duration=120) leg 3: (C, C) [1970-01-01 13:00] -> (C, C) [1970-01-01 14:07] FOOTPATH (duration=67) leg 4: (C, C) [1970-01-01 14:07] -> (END, END) [1970-01-01 14:46] MUMO (id=16, duration=39) )"; TEST(odm, prima_update) { n::timetable tt; tt.date_range_ = {date::sys_days{2017_y / January / 1}, date::sys_days{2017_y / January / 2}}; n::loader::register_special_stations(tt); auto const src = n::source_idx_t{0}; n::loader::gtfs::load_timetable({.default_tz_ = "Europe/Berlin"}, src, tt_files(), tt); n::loader::finalize(tt); auto const get_loc_idx = [&](auto&& s) { return tt.locations_.location_id_to_idx_.at({.id_ = s, .src_ = src}); }; auto const loc = osr::location{}; auto p = prima{"prima_url", loc, loc, motis::api::plan_params{}}; p.fixed_ = n::event_type::kDep; p.cap_ = {.wheelchairs_ = 1, .bikes_ = 0, .passengers_ = 1, .luggage_ = 0}; p.first_mile_taxi_ = { {get_loc_idx("A"), n::duration_t{60min}, motis::kOdmTransportModeId}, {get_loc_idx("B"), n::duration_t{60min}, motis::kOdmTransportModeId}}; p.last_mile_taxi_ = { {get_loc_idx("C"), n::duration_t{60min}, motis::kOdmTransportModeId}, {get_loc_idx("D"), n::duration_t{60min}, motis::kOdmTransportModeId}}; EXPECT_EQ(p.make_blacklist_taxi_request( tt, {n::unixtime_t{0h}, n::unixtime_t{48h}}), blacklist_request); EXPECT_FALSE(p.consume_blacklist_taxi_response(invalid_response)); EXPECT_TRUE(p.consume_blacklist_taxi_response(blacklist_response)); ASSERT_EQ(p.first_mile_taxi_.size(), 2); EXPECT_EQ(p.first_mile_taxi_[0].target_, get_loc_idx("A")); ASSERT_EQ(p.first_mile_taxi_times_[0].size(), 1); EXPECT_EQ(p.first_mile_taxi_times_[0][0].from_, to_unix(32400000)); EXPECT_EQ(p.first_mile_taxi_times_[0][0].to_, to_unix(43200000)); EXPECT_EQ(p.first_mile_taxi_[1].target_, get_loc_idx("B")); ASSERT_EQ(p.first_mile_taxi_times_[1].size(), 1); EXPECT_EQ(p.first_mile_taxi_times_[1][0].from_, to_unix(43200000)); EXPECT_EQ(p.first_mile_taxi_times_[1][0].to_, to_unix(64800000)); ASSERT_EQ(p.last_mile_taxi_.size(), 2); EXPECT_EQ(p.last_mile_taxi_[0].target_, get_loc_idx("C")); ASSERT_EQ(p.last_mile_taxi_times_[0].size(), 1); EXPECT_EQ(p.last_mile_taxi_times_[0][0].from_, to_unix(43200000)); EXPECT_EQ(p.last_mile_taxi_times_[0][0].to_, to_unix(64800000)); EXPECT_EQ(p.last_mile_taxi_[1].target_, get_loc_idx("D")); EXPECT_EQ(p.last_mile_taxi_times_[1].size(), 0); auto const expected_direct_interval = n::interval{to_unix(43200000), to_unix(64800000)}; for (auto const& d : p.direct_taxi_) { EXPECT_TRUE(expected_direct_interval.contains(d.dep_)); } auto taxi_journeys = std::vector{}; taxi_journeys.push_back( {.legs_ = {{n::direction::kForward, n::get_special_station(n::special_station::kStart), get_loc_idx("A"), n::unixtime_t{10h}, n::unixtime_t{11h}, nr::offset{get_loc_idx("A"), 1h, motis::kOdmTransportModeId}}, {n::direction::kForward, get_loc_idx("A"), n::get_special_station(n::special_station::kEnd), n::unixtime_t{11h}, n::unixtime_t{12h}, nr::offset{get_loc_idx("A"), 1h, kWalkTransportModeId}}}, .start_time_ = n::unixtime_t{10h}, .dest_time_ = n::unixtime_t{12h}, .dest_ = n::get_special_station(n::special_station::kEnd)}); taxi_journeys.push_back( {.legs_ = {{n::direction::kForward, n::get_special_station(n::special_station::kStart), get_loc_idx("B"), n::unixtime_t{11h}, n::unixtime_t{12h}, nr::offset{get_loc_idx("B"), 1h, motis::kOdmTransportModeId}}, {n::direction::kForward, get_loc_idx("B"), n::get_special_station(n::special_station::kEnd), n::unixtime_t{12h}, n::unixtime_t{13h}, nr::offset{get_loc_idx("B"), 1h, kWalkTransportModeId}}}, .start_time_ = n::unixtime_t{11h}, .dest_time_ = n::unixtime_t{13h}, .dest_ = n::get_special_station(n::special_station::kEnd)}); taxi_journeys.push_back( {.legs_ = {{n::direction::kForward, n::get_special_station(n::special_station::kStart), get_loc_idx("A"), n::unixtime_t{10h}, n::unixtime_t{11h}, n::routing::offset{get_loc_idx("A"), 1h, motis::kOdmTransportModeId}}, {n::direction::kForward, get_loc_idx("A"), get_loc_idx("C"), n::unixtime_t{11h}, n::unixtime_t{13h}, nr::offset{get_loc_idx("C"), 2h, motis::kFlexModeIdOffset}}, {n::direction::kForward, get_loc_idx("C"), n::get_special_station(n::special_station::kEnd), n::unixtime_t{13h}, n::unixtime_t{14h}, nr::offset{get_loc_idx("C"), 1h, motis::kOdmTransportModeId}}}, .start_time_ = n::unixtime_t{10h}, .dest_time_ = n::unixtime_t{14h}, .dest_ = n::get_special_station(n::special_station::kEnd)}); p.direct_taxi_ = { direct_ride{.dep_ = n::unixtime_t{11h}, .arr_ = n::unixtime_t{12h}}}; auto first_mile_taxi_rides = std::vector{}; auto last_mile_taxi_rides = std::vector{}; extract_taxis(taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides); EXPECT_FALSE(p.consume_whitelist_taxi_response( invalid_response, taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides)); EXPECT_TRUE(p.consume_whitelist_taxi_response( whitelisting_response, taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides)); auto ss = std::stringstream{}; ss << "\n"; for (auto const& j : taxi_journeys) { j.print(ss, tt, nullptr); ss << "\n"; } EXPECT_EQ(adjusted_to_whitelisting, ss.str()); } ================================================ FILE: test/odm/td_offsets_test.cc ================================================ #include "gtest/gtest.h" #include "motis/odm/td_offsets.h" #include "motis/transport_mode_ids.h" using namespace nigiri; using namespace nigiri::routing; using namespace std::chrono_literals; namespace motis::odm { void print(td_offsets_t const& tdos) { for (auto const& [l, tdo] : tdos) { std::cout << "l: " << l << ":\n"; for (auto const& t : tdo) { std::cout << "[valid_from_: " << t.valid_from_ << ", duration_: " << t.duration_ << ", transport_mode_id_: " << t.transport_mode_id_ << "]\n"; } } } TEST(odm, get_td_offsets_basic) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 1min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_extension) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 1min}, .time_at_stop_ = unixtime_t{11h + 1min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{11h + 2min}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 3min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_extension_reverse) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{11h + 2min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 1min}, .time_at_stop_ = unixtime_t{11h + 1min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 3min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_extension_fill_gap) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{11h + 2min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 1min}, .time_at_stop_ = unixtime_t{11h + 1min}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 3min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_intermittent) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{11h}, .time_at_stop_ = unixtime_t{12h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{12h}, .time_at_stop_ = unixtime_t{13h}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 6U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 1min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].valid_from_, unixtime_t{11h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].valid_from_, unixtime_t{11h + 1min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[4].valid_from_, unixtime_t{12h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[4].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[4].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[5].valid_from_, unixtime_t{12h + 1min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[5].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[5].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_long_short_long) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{11h + 2min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 1min}, .time_at_stop_ = unixtime_t{10h + 31min}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 4U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 1min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, 30min); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].valid_from_, unixtime_t{10h + 2min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].valid_from_, unixtime_t{10h + 3min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_late_improvement) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 1min}, .time_at_stop_ = unixtime_t{11h + 1min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{11h + 2min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 3min}, .time_at_stop_ = unixtime_t{11h + 3min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{10h + 32min}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 4U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 2min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, 30min); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].valid_from_, unixtime_t{10h + 3min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].valid_from_, unixtime_t{10h + 4min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].transport_mode_id_, kOdmTransportModeId); } TEST(odm, get_td_offsets_late_worse) { auto const rides = std::vector{{.time_at_start_ = unixtime_t{10h}, .time_at_stop_ = unixtime_t{11h}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 1min}, .time_at_stop_ = unixtime_t{11h + 1min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{11h + 2min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 3min}, .time_at_stop_ = unixtime_t{11h + 3min}, .stop_ = location_idx_t{1U}}, {.time_at_start_ = unixtime_t{10h + 2min}, .time_at_stop_ = unixtime_t{12h + 2min}, .stop_ = location_idx_t{1U}}}; auto const td_offsets = motis::odm::get_td_offsets(rides, kOdmTransportModeId); print(td_offsets); ASSERT_TRUE(td_offsets.contains(location_idx_t{1U})); ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_, kOdmTransportModeId); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_, unixtime_t{10h + 4min}); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, footpath::kMaxDuration); EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_, kOdmTransportModeId); } } // namespace motis::odm ================================================ FILE: test/read_test.cc ================================================ #include "gtest/gtest.h" #include "motis/parse_location.h" using namespace motis; using namespace date; namespace n = nigiri; using namespace std::chrono_literals; TEST(motis, parse_location_with_level) { auto const parsed = parse_location("-123.1,44.2,-1.5"); ASSERT_TRUE(parsed.has_value()); EXPECT_EQ((osr::location{{-123.1, 44.2}, osr::level_t{-1.5F}}), *parsed); } TEST(motis, parse_location_no_level) { auto const parsed = parse_location("-23.1,45.2"); ASSERT_TRUE(parsed.has_value()); EXPECT_EQ((osr::location{{-23.1, 45.2}, osr::kNoLevel}), *parsed); } TEST(motis, parse_cursor_earlier) { auto const q = cursor_to_query("EARLIER|1720036560"); ASSERT_TRUE( std::holds_alternative>(q.start_time_)); auto const interval = std::get>(q.start_time_); EXPECT_EQ(sys_days{2024_y / July / 3} + 17h + 56min, interval.from_); EXPECT_EQ(sys_days{2024_y / July / 3} + 19h + 56min, interval.to_); } TEST(motis, parse_cursor_later) { auto const q = cursor_to_query("LATER|1720036560"); ASSERT_TRUE( std::holds_alternative>(q.start_time_)); auto const interval = std::get>(q.start_time_); EXPECT_EQ(sys_days{2024_y / July / 3} + 19h + 56min, interval.from_); EXPECT_EQ(sys_days{2024_y / July / 3} + 21h + 56min, interval.to_); } ================================================ FILE: test/resources/gbfs/free_bike_status.json ================================================ { "last_updated": 1729500733, "ttl": 240, "version": "2.3", "data": { "bikes": [ { "bike_id": "CAB:Vehicle:887f506f-23fb-48d4-92f7-3c74446e729c", "lat": 49.87530897258006, "lon": 8.627667300924486, "is_reserved": false, "is_disabled": false, "rental_uris": { "android": "https://www.callabike.de/bike?number=12404", "ios": "https://www.callabike.de/bike?number=12404" }, "vehicle_type_id": "CAB:VehicleType:832e0956-f155-3c82-9211-c2beb9f6929d" } ] } } ================================================ FILE: test/resources/gbfs/gbfs.json ================================================ { "last_updated": 1728356899, "ttl": 86400, "version": "2.3", "data": { "de": { "feeds": [ { "name": "gbfs", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/gbfs" }, { "name": "system_information", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/system_information" }, { "name": "vehicle_types", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/vehicle_types" }, { "name": "station_information", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/station_information" }, { "name": "station_status", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/station_status" }, { "name": "free_bike_status", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/free_bike_status" }, { "name": "geofencing_zones", "url": "https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/geofencing_zones" } ] } } } ================================================ FILE: test/resources/gbfs/geofencing_zones.json ================================================ { "last_updated": 1729479604, "ttl": 86400, "version": "2.3", "data": { "geofencing_zones": { "type": "FeatureCollection", "features": [] } } } ================================================ FILE: test/resources/gbfs/station_information.json ================================================ { "last_updated": 1729479276, "ttl": 86400, "version": "2.3", "data": { "stations": [ { "station_id": "CAB:Station:4a58211d-7fc2-4897-bc9f-3767f804953a", "name": "Darmstadt Hbf", "lat": 49.871651, "lon": 8.631084 } ] } } ================================================ FILE: test/resources/gbfs/station_status.json ================================================ { "last_updated": 1729500733, "ttl": 240, "version": "2.3", "data": { "stations": [ { "station_id": "CAB:Station:4a58211d-7fc2-4897-bc9f-3767f804953a", "num_bikes_available": 48, "vehicle_types_available": [ { "vehicle_type_id": "CAB:VehicleType:832e0956-f155-3c82-9211-c2beb9f6929d", "count": 48 } ], "is_installed": true, "is_renting": true, "is_returning": true, "last_reported": 1729500733 } ] } } ================================================ FILE: test/resources/gbfs/system_information.json ================================================ { "last_updated": 1728960694, "ttl": 86400, "version": "2.3", "data": { "system_id": "callabike", "language": "de", "name": "Call a Bike", "short_name": "CaB", "operator": "Deutsche Bahn Connect GmbH", "url": "https://www.callabike.de", "feed_contact_email": "doServices.Sirius.Team@deutschebahn.com", "timezone": "Europe/Berlin", "rental_apps": { "android": { "store_uri": "https://play.google.com/store/apps/details?id=de.bahn.callabike", "discovery_uri": "callabike://" }, "ios": { "store_uri": "https://apps.apple.com/de/app/call-a-bike/id420360589", "discovery_uri": "callabike://" } } } } ================================================ FILE: test/resources/gbfs/vehicle_types.json ================================================ { "last_updated": 1729497561, "ttl": 86400, "version": "2.3", "data": { "vehicle_types": [ { "vehicle_type_id": "CAB:VehicleType:832e0956-f155-3c82-9211-c2beb9f6929d", "form_factor": "bicycle", "propulsion_type": "human", "name": "bike", "return_constraint": "hybrid" } ] } } ================================================ FILE: test/resources/ojp/geocoding_request.xml ================================================ de 2026-01-28T07:22:00.862Z OJP_DemoApp_Beta_OJP2.0 2026-01-28T07:22:00.862Z Darmstadt Hauptbahnhof stop 10 true ================================================ FILE: test/resources/ojp/geocoding_response.xml ================================================ NOW MOTIS MSG NOW de ================================================ FILE: test/resources/ojp/intermodal_routing_request.xml ================================================ de 2019-05-01T01:15Z OJP_DemoApp_Beta_OJP2.0 2019-05-01T01:15Z 8.6586978 50.1040763 n/a 2019-05-01T01:15Z 8.6767235 50.1132737 n/a 2 true true true true ================================================ FILE: test/resources/ojp/intermodal_routing_response.xml ================================================ 2026-02-16T11:08:06.083Z MOTIS 6 2026-02-16T11:08:06.083Z de test_FFM FFM Hbf FFM Hbf 8.66341 50.10701 test_de:6412:10:6:1 FFM Hbf test_FFM FFM Hbf 8.66382 50.10758 test_FFM_HAUPT FFM Hauptwache FFM Hauptwache 8.67835 50.11403 test_FFM_HAUPT_U FFM Hauptwache test_FFM_HAUPT FFM Hauptwache 8.67912 50.11385 test_FFM_101 FFM Hbf test_FFM FFM Hbf 8.66333 50.10739 test_FFM_HAUPT_S FFM Hauptwache test_FFM_HAUPT FFM Hauptwache 8.67824 50.11404 1 1 PT19M 2019-05-01T01:55:00Z 2019-05-01T02:14:00Z 0 102 1 PT10M 8.6587 50.10408 START test_de:6412:10:6:1 FFM Hbf own foot PT10M 57 8.6587 50.10408 START test_de:6412:10:6:1 FFM Hbf 8.65869 50.10409 8.65907 50.1042 8.65932 50.10428 8.65932 50.10428 8.65999 50.10452 8.66032 50.10465 8.66119 50.10499 8.66189 50.10527 8.6626 50.10554 8.66337 50.10584 8.66379 50.10601 8.66379 50.10601 8.66386 50.10604 8.66386 50.10604 8.66379 50.10611 8.66379 50.10611 8.66374 50.10616 8.66374 50.10616 8.66372 50.10618 8.66372 50.10618 8.66368 50.10622 8.66368 50.10622 8.66365 50.10626 8.66365 50.10626 8.66384 50.10633 8.66384 50.10633 8.6637 50.10648 8.6637 50.10648 8.66353 50.10666 8.66353 50.10666 8.66345 50.10674 8.66345 50.10674 8.66332 50.10688 8.66332 50.10688 8.66375 50.10705 8.66375 50.10705 8.66378 50.10706 8.66378 50.10706 8.66372 50.10714 8.66372 50.10714 8.66375 50.10713 8.66375 50.10713 8.66389 50.10719 8.66389 50.10719 8.66397 50.10724 8.66397 50.10724 8.66406 50.10727 8.66406 50.10727 8.6641 50.10729 8.6641 50.10729 8.66405 50.10731 8.66405 50.10731 8.66399 50.10737 8.66399 50.10737 8.66397 50.10736 8.6639 50.10744 8.66377 50.10756 PT10M 57 2 PT5M test_de:6412:10:6:1 FFM Hbf U4 2019-05-01T02:05:00Z 2019-05-01T02:05:00Z 1 test_FFM_HAUPT_U FFM Hauptwache 2019-05-01T02:10:00Z 2019-05-01T02:10:00Z 2 20190501 20190501_04:05_test_U4 test_U4 0 DB FFM Hauptwache U4 metro tube test_de:6412:10:6:1 FFM Hbf test_FFM_HAUPT_U FFM Hauptwache 8.66382 50.10758 8.67912 50.11385 PT5M 2 3 PT4M test_FFM_HAUPT_U FFM Hauptwache 8.67672 50.11327 END own foot PT4M 43 test_FFM_HAUPT_U FFM Hauptwache 8.67672 50.11327 END 8.67918 50.11384 8.67917 50.1138 8.67913 50.11376 8.67913 50.11376 8.6791 50.11376 8.6791 50.11376 8.67897 50.11347 8.67884 50.11329 8.67885 50.11329 8.67885 50.11329 8.6788 50.11322 8.6788 50.11322 8.67876 50.11315 8.67876 50.11315 8.67864 50.11321 8.67864 50.11321 8.67828 50.11337 8.67828 50.11337 8.67812 50.1134 8.67812 50.1134 8.67796 50.11342 8.67796 50.11342 8.67792 50.11342 8.67792 50.11342 8.67793 50.11346 8.67793 50.11346 8.67756 50.11352 8.67756 50.11352 8.67706 50.1136 8.67706 50.1136 8.67696 50.11361 8.67696 50.11361 8.67693 50.11333 8.67693 50.11333 8.67683 50.11334 8.67682 50.11332 8.67682 50.11332 8.6768 50.11327 8.6768 50.11327 8.67676 50.11327 8.67673 50.11328 8.67673 50.11328 8.67673 50.11327 PT4M 43 2 2 PT19M 2019-05-01T02:05:00Z 2019-05-01T02:24:00Z 0 110 1 PT10M 8.6587 50.10408 START test_FFM_101 FFM Hbf own foot PT10M 80 8.6587 50.10408 START test_FFM_101 FFM Hbf 8.65869 50.10409 8.65907 50.1042 8.65932 50.10428 8.65932 50.10428 8.65999 50.10452 8.66032 50.10465 8.66119 50.10499 8.66189 50.10527 8.6626 50.10554 8.66337 50.10584 8.66379 50.10601 8.66379 50.10601 8.66386 50.10604 8.66386 50.10604 8.66379 50.10611 8.66379 50.10611 8.66374 50.10616 8.66374 50.10616 8.66372 50.10618 8.66372 50.10618 8.66368 50.10622 8.66368 50.10622 8.66365 50.10626 8.66365 50.10626 8.66364 50.10627 8.66364 50.10627 8.66357 50.10634 8.66357 50.10634 8.66352 50.10639 8.66352 50.10639 8.66351 50.10641 8.66351 50.10641 8.66345 50.10646 8.66345 50.10646 8.6634 50.10652 8.6634 50.10652 8.66333 50.1066 8.66333 50.1066 8.66326 50.10667 8.66326 50.10667 8.66325 50.10668 8.66325 50.10668 8.66318 50.10676 8.66318 50.10676 8.6631 50.10683 8.6631 50.10683 8.66306 50.10687 8.66306 50.10687 8.66304 50.1069 8.66304 50.1069 8.66301 50.10693 8.66297 50.10697 8.66297 50.10697 8.6629 50.10704 8.6629 50.10704 8.66285 50.1071 8.66285 50.1071 8.66293 50.10714 8.66293 50.10714 8.6629 50.10714 8.6629 50.10714 8.66283 50.10722 8.6628 50.10721 8.6628 50.10721 8.66278 50.1072 8.66268 50.10731 8.66268 50.10731 8.66262 50.10729 8.66262 50.10729 8.66257 50.10715 8.66257 50.10715 8.66255 50.10712 8.66255 50.10712 8.66241 50.10706 8.66241 50.10706 8.66244 50.10703 8.66244 50.10703 8.66297 50.10724 8.66297 50.10724 8.66333 50.10739 PT10M 80 2 PT5M test_FFM_101 FFM Hbf 101 2019-05-01T02:15:00Z 2019-05-01T02:15:00Z 1 test_FFM_HAUPT_S FFM Hauptwache 2019-05-01T02:20:00Z 2019-05-01T02:20:00Z 2 20190501 20190501_04:15_test_S3 test_S3 0 DB FFM Hauptwache S3 rail suburbanRailway test_FFM_101 FFM Hbf test_FFM_HAUPT_S FFM Hauptwache 8.66333 50.10739 8.67824 50.11404 PT5M 2 3 PT4M test_FFM_HAUPT_S FFM Hauptwache 8.67672 50.11327 END own foot PT4M 28 test_FFM_HAUPT_S FFM Hauptwache 8.67672 50.11327 END 8.67824 50.11404 8.67823 50.11404 8.67823 50.11404 8.67819 50.11404 8.67819 50.11404 8.6782 50.11412 8.6782 50.11412 8.67762 50.11416 8.67707 50.11415 8.67671 50.11412 8.67671 50.11412 8.67669 50.11398 8.67669 50.11398 8.67668 50.11397 8.67665 50.11385 8.67665 50.11385 8.67662 50.11375 8.67662 50.11375 8.67661 50.11373 8.67661 50.11373 8.67658 50.11364 8.67658 50.11364 8.67672 50.11363 8.67679 50.11359 8.67679 50.11351 8.67673 50.11328 8.67673 50.11328 8.67673 50.11327 PT4M 28 ================================================ FILE: test/resources/ojp/map_stops_request.xml ================================================ de 2026-01-28T07:47:11.377Z OJP_DemoApp_Beta_OJP2.0 2026-01-28T07:47:11.377Z 49.87400 8.62850 49.87100 8.63250 stop 300 true ================================================ FILE: test/resources/ojp/map_stops_response.xml ================================================ NOW MOTIS MSG NOW de test_DA DA Hbf DA Hbf 8.63085 49.8726 rail highSpeedRail true 1 test_DA_3 DA Hbf DA Hbf 8.63003 49.87355 rail highSpeedRail true 1 test_DA_10 DA Hbf DA Hbf 8.62926 49.87336 rail highSpeedRail true 1 ================================================ FILE: test/resources/ojp/routing_request.xml ================================================ de 2019-05-01T00:30:00Z OJP_DemoApp_Beta_OJP2.0 2019-05-01T00:30:00Z test_DA n/a 2019-05-01T00:30:00Z test_FFM n/a 5 true true true true true explanatory ================================================ FILE: test/resources/ojp/routing_response.xml ================================================ NOW MOTIS MSG NOW de test_DA DA Hbf DA Hbf 8.63085 49.8726 test_DA_10 DA Hbf test_DA DA Hbf 8.62926 49.87336 test_FFM FFM Hbf FFM Hbf 8.66341 50.10701 test_FFM_10 FFM Hbf test_FFM FFM Hbf 8.66118 50.10593 1 1 PT10M 2019-05-01T00:35:00Z 2019-05-01T00:45:00Z 0 2 1 PT10M test_DA_10 DA Hbf 10 2019-05-01T00:35:00Z 2019-05-01T00:35:00Z 1 test_FFM_10 FFM Hbf 10 2019-05-01T00:45:00Z 2019-05-01T00:45:00Z 2 20190501 20190501_02:35_test_ICE test_ICE 0 DB FFM Hbf ICE rail highSpeedRail test_DA_10 DA Hbf test_FFM_10 FFM Hbf PT10M 2 2 2 PT10M 2019-05-01T01:35:00Z 2019-05-01T01:45:00Z 0 2 1 PT10M test_DA_10 DA Hbf 10 2019-05-01T01:35:00Z 2019-05-01T01:35:00Z 1 test_FFM_10 FFM Hbf 10 2019-05-01T01:45:00Z 2019-05-01T01:45:00Z 2 20190501 20190501_03:35_test_ICE test_ICE 0 DB FFM Hbf ICE rail highSpeedRail test_DA_10 DA Hbf test_FFM_10 FFM Hbf PT10M 2 3 3 PT10M 2019-05-01T02:35:00Z 2019-05-01T02:45:00Z 0 2 1 PT10M test_DA_10 DA Hbf 10 2019-05-01T02:35:00Z 2019-05-01T02:35:00Z 1 test_FFM_10 FFM Hbf 10 2019-05-01T02:45:00Z 2019-05-01T02:45:00Z 2 20190501 20190501_04:35_test_ICE test_ICE 0 DB FFM Hbf ICE rail highSpeedRail test_DA_10 DA Hbf test_FFM_10 FFM Hbf PT10M 2 4 4 PT10M 2019-05-01T03:35:00Z 2019-05-01T03:45:00Z 0 2 1 PT10M test_DA_10 DA Hbf 10 2019-05-01T03:35:00Z 2019-05-01T03:35:00Z 1 test_FFM_10 FFM Hbf 10 2019-05-01T03:45:00Z 2019-05-01T03:45:00Z 2 20190501 20190501_05:35_test_ICE test_ICE 0 DB FFM Hbf ICE rail highSpeedRail test_DA_10 DA Hbf test_FFM_10 FFM Hbf PT10M 2 5 5 PT10M 2019-05-01T04:35:00Z 2019-05-01T04:45:00Z 0 2 1 PT10M test_DA_10 DA Hbf 10 2019-05-01T04:35:00Z 2019-05-01T04:35:00Z 1 test_FFM_10 FFM Hbf 10 2019-05-01T04:45:00Z 2019-05-01T04:45:00Z 2 20190501 20190501_06:35_test_ICE test_ICE 0 DB FFM Hbf ICE rail highSpeedRail test_DA_10 DA Hbf test_FFM_10 FFM Hbf PT10M 2 ================================================ FILE: test/resources/ojp/stop_event_request.xml ================================================ de 2026-01-28T07:38:53.987Z OJP_DemoApp_Beta_OJP2.0 2026-01-28T07:38:53.987Z test_DA_3 n/a 2026-01-28T06:00:00.000Z true 10 departure true true explanatory ================================================ FILE: test/resources/ojp/stop_event_response.xml ================================================ NOW MOTIS MSG NOW de ================================================ FILE: test/resources/ojp/trip_info_request.xml ================================================ de 2026-01-28T07:42:51.310Z OJP_DemoApp_Beta_OJP2.0 2026-01-28T07:42:51.310Z 20190501_00:35_test_ICE 2019-05-01 true true true true true ================================================ FILE: test/resources/ojp/trip_info_response.xml ================================================ NOW MOTIS MSG NOW de test_DA DA Hbf DA Hbf 8.63085 49.8726 test_DA_10 DA Hbf test_DA DA Hbf 8.62926 49.87336 test_FFM FFM Hbf FFM Hbf 8.66341 50.10701 test_FFM_10 FFM Hbf test_FFM FFM Hbf 8.66118 50.10593 test_DA_10 DA Hbf 10 PLATFORM_ACCESS_WITHOUT_ASSISTANCE 2019-04-30T22:35:00Z 2019-04-30T22:35:00Z 1 test_FFM_10 FFM Hbf 10 PLATFORM_ACCESS_WITHOUT_ASSISTANCE 2019-04-30T22:45:00Z 2019-04-30T22:45:00Z 2 2019-05-01 20190501_00:35_test_ICE ICE test_ICE 0 rail highSpeedRail ICE DA Hbf DB test_FFM_10 FFM Hbf test_DA_10 DA Hbf test_FFM_10 FFM Hbf PT10M 0 ================================================ FILE: test/resources/test_case.geojson ================================================ { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {}, "geometry": { "coordinates": [ [ [ [ 8.628286687808867, 49.87538775650398 ], [ 8.627729587531803, 49.875307377823475 ], [ 8.627688012884107, 49.87204389041574 ], [ 8.629583816813295, 49.8703022017942 ], [ 8.631820532852942, 49.87093993507034 ], [ 8.632028406090228, 49.87524843337286 ], [ 8.628286687808867, 49.87538775650398 ] ] ], [ [ [ 8.659725253456145, 50.10759331995783 ], [ 8.656418533851479, 50.10612497067021 ], [ 8.656811133317234, 50.10409793307585 ], [ 8.660305268566447, 50.103896483660066 ], [ 8.667076092035131, 50.105113219822016 ], [ 8.666971080055504, 50.10788252455177 ], [ 8.662900321548136, 50.10907497984371 ], [ 8.659725253456145, 50.10759331995783 ] ] ], [ [ [ 8.675250073298457, 50.11486984518109 ], [ 8.675183699890653, 50.11167759014242 ], [ 8.678274804325156, 50.111671509453686 ], [ 8.68149865557524, 50.11412196451832 ], [ 8.675250073298457, 50.11486984518109 ] ] ] ], "type": "MultiPolygon" } } ] } ================================================ FILE: test/routing_shrink_results_test.cc ================================================ #include "gtest/gtest.h" #include "motis/endpoints/routing.h" using namespace std::chrono_literals; using namespace date; using namespace motis::ep; namespace n = nigiri; using iv = n::interval; TEST(motis, shrink) { auto const d = date::sys_days{2025_y / September / 29}; { auto const j = [](std::uint8_t const transfers, n::unixtime_t const dep, n::unixtime_t const arr) { auto x = n::routing::journey{}; x.start_time_ = dep; x.dest_time_ = arr; x.transfers_ = transfers; return x; }; auto journeys = std::vector{ j(2, d + 10h, d + 11h), // j(1, d + 10h, d + 12h), // j(0, d + 10h, d + 13h), // j(2, d + 20h, d + 21h), // j(1, d + 20h, d + 22h), // j(0, d + 20h, d + 23h), // }; auto const i4 = shrink(false, 4, {d + 11h, d + 20h}, journeys); EXPECT_EQ(6, journeys.size()); EXPECT_EQ((iv{d + 11h, d + 20h}), i4); auto const i3 = shrink(false, 3, {d + 11h, d + 13h}, journeys); EXPECT_EQ(3, journeys.size()); EXPECT_EQ((iv{d + 11h, d + 20h}), i3); auto const i2 = shrink(false, 2, {d + 11h, d + 13h}, journeys); EXPECT_EQ(3, journeys.size()); EXPECT_EQ((iv{d + 11h, d + 13h}), i2); auto const i1 = shrink(false, 1, {d + 11h, d + 13h}, journeys); EXPECT_EQ(3, journeys.size()); EXPECT_EQ((iv{d + 11h, d + 13h}), i1); } { auto const j = [](std::uint8_t const transfers, n::unixtime_t const dep, n::unixtime_t const arr) { auto x = n::routing::journey{}; x.start_time_ = arr; x.dest_time_ = dep; x.transfers_ = transfers; return x; }; auto journeys = std::vector{ j(2, d + 10h, d + 11h), j(1, d + 10h, d + 12h), j(0, d + 10h, d + 13h), }; auto const i3 = shrink(true, 3, {d + 11h, d + 13h}, journeys); EXPECT_EQ(3, journeys.size()); EXPECT_EQ((iv{d + 11h, d + 13h}), i3); auto const i2 = shrink(true, 2, {d + 11h, d + 13h}, journeys); EXPECT_EQ((std::vector{ j(1, d + 10h, d + 12h), j(0, d + 10h, d + 13h), }), journeys); EXPECT_EQ(2, journeys.size()); EXPECT_EQ((iv{d + 11h + 1min, d + 13h}), i2); auto const i1 = shrink(true, 1, {d + 11h, d + 13h}, journeys); EXPECT_EQ((std::vector{ j(0, d + 10h, d + 13h), }), journeys); EXPECT_EQ(1, journeys.size()); EXPECT_EQ((iv{d + 12h + 1min, d + 13h}), i1); } } ================================================ FILE: test/routing_slow_direct_test.cc ================================================ #include "gtest/gtest.h" #include "utl/init_from.h" #include "motis-api/motis-api.h" #include "motis/config.h" #include "motis/endpoints/routing.h" #include "motis/import.h" #include "./util.h" using namespace std::string_view_literals; using namespace motis; using namespace date; using namespace std::chrono_literals; constexpr auto const kSlowDirectGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type ICE,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id ICE,S1,ICE,, ICE,S1,ICE2,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type ICE,00:35:00,00:35:00,DA_10,0,0,1 ICE,00:45:00,00:45:00,FFM_10,1,1,0 ICE2,00:35:00,00:35:00,DA_10,0,0,1 ICE2,00:45:00,00:45:00,FFM_10,1,1,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 # frequencies.txt trip_id,start_time,end_time,headway_secs ICE,00:35:00,24:35:00,3600 ICE2,00:35:00,24:35:00,3600 )"sv; TEST(motis, routing_slow_direct) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{ .server_ = {{.web_folder_ = "ui/build", .n_threads_ = 1U}}, .osm_ = {"test/resources/test_case.osm.pbf"}, .tiles_ = {{.profile_ = "deps/tiles/profile/full.lua", .db_size_ = 1024U * 1024U * 25U}}, .timetable_ = config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .use_osm_stop_coordinates_ = true, .extend_missing_footpaths_ = false, .datasets_ = {{"test", {.path_ = std::string{kSlowDirectGTFS}}}}}, .gbfs_ = {{.feeds_ = {{"CAB", {.url_ = "./test/resources/gbfs"}}}}}, .street_routing_ = true, .osr_footpath_ = true, .geocoding_ = true, .reverse_geocoding_ = true}; import(c, "test/data_osm_only"); auto d = data{"test/data_osm_only", c}; auto const routing = utl::init_from(d).value(); { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=test_FFM_10" "&time=2019-05-01T01:30Z" "&slowDirect=false"); ASSERT_TRUE(res.itineraries_.size() >= 2); EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_, "20190501_03:35_test_ICE2"); EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_, "20190501_04:35_test_ICE2"); } { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=test_FFM_10" "&time=2019-05-01T01:30Z" "&slowDirect=true"); ASSERT_TRUE(res.itineraries_.size() >= 2); EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_, "20190501_03:35_test_ICE2"); EXPECT_EQ( res.itineraries_.at(1).legs_.at(1).tripId_, "20190501_03:35_test_ICE2"); // this contains an addional 1min footpath EXPECT_EQ(res.itineraries_.at(2).legs_.at(1).tripId_, "20190501_03:35_test_ICE"); } { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=test_FFM_10" "&time=2019-05-01T01:30Z" "&slowDirect=true" "&arriveBy=true" "&numItineraries=2&maxItineraries=2"); ASSERT_TRUE(res.itineraries_.size() >= 2); EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_, "20190501_02:35_test_ICE2"); EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_, "20190501_02:35_test_ICE"); } { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=test_FFM_10" "&time=2019-05-01T01:30Z" "&slowDirect=true" "&numItineraries=2&maxItineraries=2" "&pageCursor=EARLIER%7C1556674200"); ASSERT_TRUE(res.itineraries_.size() >= 2); EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_, "20190501_02:35_test_ICE2"); EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_, "20190501_02:35_test_ICE"); } { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=test_FFM_10" "&time=2019-05-01T01:30Z" "&slowDirect=true" "&arriveBy=true" "&numItineraries=2&maxItineraries=2" "&pageCursor=LATER%7C1556674200"); ASSERT_TRUE(res.itineraries_.size() >= 2); EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_, "20190501_03:35_test_ICE2"); EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_, "20190501_03:35_test_ICE"); } } ================================================ FILE: test/routing_test.cc ================================================ #include "gtest/gtest.h" #include #include #include "boost/asio/co_spawn.hpp" #include "boost/asio/detached.hpp" #include "boost/json.hpp" #ifdef NO_DATA #undef NO_DATA #endif #include "gtfsrt/gtfs-realtime.pb.h" #include "utl/init_from.h" #include "nigiri/rt/gtfsrt_update.h" #include "motis-api/motis-api.h" #include "motis/config.h" #include "motis/data.h" #include "motis/elevators/elevators.h" #include "motis/elevators/parse_fasta.h" #include "motis/endpoints/routing.h" #include "motis/gbfs/update.h" #include "motis/import.h" #include "./util.h" namespace json = boost::json; using namespace std::string_view_literals; using namespace motis; using namespace date; using namespace std::chrono_literals; namespace n = nigiri; constexpr auto const kFastaJson = R"__( [ { "description": "FFM HBF zu Gleis 101/102 (S-Bahn)", "equipmentnumber" : 10561326, "geocoordX" : 8.6628995, "geocoordY" : 50.1072933, "operatorname" : "DB InfraGO", "state" : "ACTIVE", "stateExplanation" : "available", "stationnumber" : 1866, "type" : "ELEVATOR", "outOfService": [ ["2019-05-01T01:30:00Z", "2019-05-01T02:30:00Z"] ] }, { "description": "FFM HBF zu Gleis 103/104 (S-Bahn)", "equipmentnumber": 10561327, "geocoordX": 8.6627516, "geocoordY": 50.1074549, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1866, "type": "ELEVATOR" }, { "description": "HAUPTWACHE zu Gleis 2/3 (S-Bahn)", "equipmentnumber": 10351032, "geocoordX": 8.67818, "geocoordY": 50.114046, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1864, "type": "ELEVATOR" }, { "description": "DA HBF zu Gleis 1", "equipmentnumber": 10543458, "geocoordX": 8.6303864, "geocoordY": 49.8725612, "state": "ACTIVE", "type": "ELEVATOR" }, { "description": "DA HBF zu Gleis 3/4", "equipmentnumber": 10543453, "geocoordX": 8.6300911, "geocoordY": 49.8725678, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1126, "type": "ELEVATOR" }, { "description": "zu Gleis 5/6", "equipmentnumber": 10543454, "geocoordX": 8.6298163, "geocoordY": 49.8725555, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1126, "type": "ELEVATOR" }, { "description": "zu Gleis 7/8", "equipmentnumber": 10543455, "geocoordX": 8.6295535, "geocoordY": 49.87254, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1126, "type": "ELEVATOR" }, { "description": "zu Gleis 9/10", "equipmentnumber": 10543456, "geocoordX": 8.6293117, "geocoordY": 49.8725263, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1126, "type": "ELEVATOR" }, { "description": "zu Gleis 11/12", "equipmentnumber": 10543457, "geocoordX": 8.6290451, "geocoordY": 49.8725147, "operatorname": "DB InfraGO", "state": "ACTIVE", "stateExplanation": "available", "stationnumber": 1126, "type": "ELEVATOR" } ] )__"sv; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA_10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, FFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3,DB,S3,,,109 U4,DB,U4,,,402 ICE,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3,S1,S3,, U4,S1,U4,, ICE,S1,ICE,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type S3,01:15:00,01:15:00,FFM_101,1,0,0 S3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0 U4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 U4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0 ICE,00:35:00,00:35:00,DA_10,0,0,0 ICE,00:45:00,00:45:00,FFM_10,1,0,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 # frequencies.txt trip_id,start_time,end_time,headway_secs S3,01:15:00,25:15:00,3600 ICE,00:35:00,24:35:00,3600 U4,01:05:00,25:01:00,3600 )"sv; void print_short(std::ostream& out, api::Itinerary const& j) { auto const format_time = [&](auto&& t, char const* fmt = "%F %H:%M") { out << date::format(fmt, *t); }; auto const format_duration = [&](auto&& t, char const* fmt = "%H:%M") { out << date::format(fmt, std::chrono::milliseconds{t}); }; out << "date="; format_time(j.startTime_, "%F"); out << ", start="; format_time(j.startTime_, "%H:%M"); out << ", end="; format_time(j.endTime_, "%H:%M"); out << ", duration="; format_duration(j.duration_ * 1000U); out << ", transfers=" << j.transfers_; out << ", legs=[\n"; auto first = true; for (auto const& leg : j.legs_) { if (!first) { out << ",\n "; } else { out << " "; } first = false; auto const print_alerts = [&](auto&& x) { if (x.alerts_) { auto first_alert = true; out << ", alerts=["; for (auto const& a : *x.alerts_) { if (!first_alert) { out << ", "; } first_alert = false; out << "\"" << a.headerText_ << "\""; } out << "]"; } }; auto const print_stop = [&](api::Place const& p) { out << p.stopId_.value_or("-") << " [track=" << p.track_.value_or("-") << ", scheduled_track=" << p.scheduledTrack_.value_or("-") << ", level=" << p.level_; print_alerts(p); out << "]"; }; out << "("; out << "from="; print_stop(leg.from_); out << ", to="; print_stop(leg.to_); out << ", start="; format_time(leg.startTime_); out << ", mode=\"" << leg.mode_ << "\", trip=\"" << leg.routeShortName_.value_or("-") << "\""; out << ", end="; format_time(leg.endTime_); print_alerts(leg); out << ")"; } out << "\n]"; } std::string to_str(std::vector const& x) { auto ss = std::stringstream{}; for (auto const& j : x) { print_short(ss, j); } return ss.str(); } TEST(motis, routing_osm_only_direct_walk) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data_osm_only", ec); auto const c = config{.server_ = {{.web_folder_ = "ui/build", .n_threads_ = 1U}}, .osm_ = {"test/resources/test_case.osm.pbf"}, .street_routing_ = true}; import(c, "test/data_osm_only"); auto d = data{"test/data_osm_only", c}; auto const routing = utl::init_from(d).value(); auto const res = routing( "?fromPlace=49.87526849014631,8.62771903392948" "&toPlace=49.87253873915287,8.629724234688751" "&time=2019-05-01T01:25Z" "&timetableView=false" "&transitModes=" "&directModes=WALK"); ASSERT_TRUE(res.itineraries_.empty()); ASSERT_EQ(1U, res.direct_.size()); ASSERT_EQ(1U, res.direct_.front().legs_.size()); EXPECT_EQ(api::ModeEnum::WALK, res.direct_.front().legs_.front().mode_); } TEST(motis, routing) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = config{ .server_ = {{.web_folder_ = "ui/build", .n_threads_ = 1U}}, .osm_ = {"test/resources/test_case.osm.pbf"}, .tiles_ = {{.profile_ = "deps/tiles/profile/full.lua", .db_size_ = 1024U * 1024U * 25U}}, .timetable_ = config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .use_osm_stop_coordinates_ = true, .extend_missing_footpaths_ = false, .datasets_ = {{"test", {.path_ = std::string{kGTFS}}}}}, .gbfs_ = {{.feeds_ = {{"CAB", {.url_ = "./test/resources/gbfs"}}}}}, .street_routing_ = true, .osr_footpath_ = true, .geocoding_ = true, .reverse_geocoding_ = true}; import(c, "test/data"); auto d = data{"test/data", c}; d.rt_->e_ = std::make_unique(*d.w_, nullptr, *d.elevator_nodes_, parse_fasta(kFastaJson)); d.init_rtt(date::sys_days{2019_y / May / 1}); { auto ioc = boost::asio::io_context{}; boost::asio::co_spawn( ioc, [&]() -> boost::asio::awaitable { co_await gbfs::update(c, *d.w_, *d.l_, d.gbfs_); }, boost::asio::detached); ioc.run(); } auto const stats = n::rt::gtfsrt_update_msg( *d.tt_, *d.rt_->rtt_, n::source_idx_t{0}, "test", test::to_feed_msg( {test::trip_update{.trip_ = {.trip_id_ = "ICE", .start_time_ = {"03:35:00"}, .date_ = {"20190501"}}, .stop_updates_ = {{.stop_id_ = "FFM_12", .seq_ = std::optional{1U}, .ev_type_ = n::event_type::kArr, .delay_minutes_ = 10, .stop_assignment_ = "FFM_12"}}}, test::alert{.header_ = "Yeah", .description_ = "Yeah!!", .entities_ = {{.trip_ = {{.trip_id_ = "ICE", .start_time_ = {"03:35:00"}, .date_ = {"20190501"}}}, .stop_id_ = "DA"}}}, test::alert{.header_ = "Hello", .description_ = "World", .entities_ = {{.trip_ = {{.trip_id_ = "ICE", .start_time_ = {"03:35:00"}, .date_ = {"20190501"}}}}}}}, date::sys_days{2019_y / May / 1} + 9h)); EXPECT_EQ(1U, stats.total_entities_success_); EXPECT_EQ(2U, stats.alert_total_resolve_success_); auto const routing = utl::init_from(d).value(); EXPECT_EQ(d.rt_->rtt_.get(), routing.rt_->rtt_.get()); // Route direct with GBFS. { auto const res = routing( "?fromPlace=49.87526849014631,8.62771903392948" "&toPlace=49.87253873915287,8.629724234688751" "&time=2019-05-01T01:25Z" "&timetableView=false" "&directModes=WALK,RENTAL"); ASSERT_FALSE(res.direct_.empty()); ASSERT_FALSE(res.direct_.front().legs_.empty()); EXPECT_GT(res.direct_.front().legs_.front().legGeometry_.length_, 0); EXPECT_TRUE(res.direct_.front().legs_.front().steps_.has_value()); EXPECT_EQ( R"(date=2019-05-01, start=01:25, end=01:36, duration=00:11, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 01:25, mode="WALK", trip="-", end=2019-05-01 01:26), (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 01:26, mode="RENTAL", trip="-", end=2019-05-01 01:27), (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 01:27, mode="WALK", trip="-", end=2019-05-01 01:36) ]date=2019-05-01, start=01:25, end=01:36, duration=00:11, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 01:25, mode="WALK", trip="-", end=2019-05-01 01:36) ])", to_str(res.direct_)); auto const compact_res = routing( "?fromPlace=49.87526849014631,8.62771903392948" "&toPlace=49.87253873915287,8.629724234688751" "&time=2019-05-01T01:25Z" "&timetableView=false" "&directModes=WALK,RENTAL" "&detailedLegs=false"); ASSERT_FALSE(compact_res.direct_.empty()); for (auto const& itinerary : compact_res.direct_) { for (auto const& leg : itinerary.legs_) { EXPECT_EQ("", leg.legGeometry_.points_); EXPECT_EQ(0, leg.legGeometry_.length_); EXPECT_FALSE(leg.steps_.has_value()); } } } // Route with GBFS. { auto const res = routing( "?fromPlace=49.875308,8.6276673" "&toPlace=50.11347,8.67664" "&time=2019-05-01T01:21Z" "&timetableView=false" "&useRoutedTransfers=true" "&preTransitModes=WALK,RENTAL"); EXPECT_EQ( R"(date=2019-05-01, start=01:21, end=02:15, duration=00:54, transfers=1, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 01:21, mode="WALK", trip="-", end=2019-05-01 01:23), (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 01:23, mode="RENTAL", trip="-", end=2019-05-01 01:24), (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 01:24, mode="WALK", trip="-", end=2019-05-01 01:35), (from=test_DA_10 [track=10, scheduled_track=10, level=-1, alerts=["Yeah"]], to=test_FFM_12 [track=12, scheduled_track=10, level=0], start=2019-05-01 01:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 01:55, alerts=["Hello"]), (from=test_FFM_12 [track=12, scheduled_track=10, level=0], to=test_de:6412:10:6:1 [track=U4, scheduled_track=U4, level=-2], start=2019-05-01 01:55, mode="WALK", trip="-", end=2019-05-01 02:00), (from=test_de:6412:10:6:1 [track=U4, scheduled_track=U4, level=-2], to=test_FFM_HAUPT_U [track=-, scheduled_track=-, level=-4], start=2019-05-01 02:05, mode="SUBWAY", trip="U4", end=2019-05-01 02:10), (from=test_FFM_HAUPT_U [track=-, scheduled_track=-, level=-4], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 02:10, mode="WALK", trip="-", end=2019-05-01 02:15) ])", to_str(res.itineraries_)); } // Routing with temporary blocked paths due to elevator being out of service // Queries will use the wheelchair profile and have a long walking path before // or after the elevator, to identify possible bugs { // Blocked near fromPlace, arriveBy=false, pass before blocked { auto const res = routing( "?fromPlace=50.1040763,8.6586978" "&toPlace=50.1132737,8.6767235" "&time=2019-05-01T01:15Z" "&arriveBy=false" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=01:16, end=02:29, duration=01:14, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_FFM_101 [track=101, scheduled_track=101, level=-3], start=2019-05-01 01:16, mode="WALK", trip="-", end=2019-05-01 01:30), (from=test_FFM_101 [track=101, scheduled_track=101, level=-3], to=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:15, mode="METRO", trip="S3", end=2019-05-01 02:20), (from=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 02:20, mode="WALK", trip="-", end=2019-05-01 02:29) ])", to_str(res.itineraries_)); } // Blocked near fromPlace, arriveBy=false, temporary blocked / must wait { auto const res = routing( "?fromPlace=50.1040763,8.6586978" "&toPlace=50.1132737,8.6767235" "&time=2019-05-01T01:20Z" "&arriveBy=false" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=03:01, end=03:29, duration=02:09, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_FFM_101 [track=101, scheduled_track=101, level=-3], start=2019-05-01 03:01, mode="WALK", trip="-", end=2019-05-01 03:15), (from=test_FFM_101 [track=101, scheduled_track=101, level=-3], to=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], start=2019-05-01 03:15, mode="METRO", trip="S3", end=2019-05-01 03:20), (from=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 03:20, mode="WALK", trip="-", end=2019-05-01 03:29) ])", to_str(res.itineraries_)); } // Blocked near fromPlace, arriveBy=true, must pass before blocked { auto const res = routing( "?fromPlace=50.1040763,8.6586978" "&toPlace=50.1132737,8.6767235" "&time=2019-05-01T02:30Z" "&arriveBy=true" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=01:16, end=02:29, duration=01:14, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_FFM_101 [track=101, scheduled_track=101, level=-3], start=2019-05-01 01:16, mode="WALK", trip="-", end=2019-05-01 01:30), (from=test_FFM_101 [track=101, scheduled_track=101, level=-3], to=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:15, mode="METRO", trip="S3", end=2019-05-01 02:20), (from=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 02:20, mode="WALK", trip="-", end=2019-05-01 02:29) ])", to_str(res.itineraries_)); } // Blocked near fromPlace, arriveBy=true, can pass after blocked { auto const res = routing( "?fromPlace=50.1040763,8.6586978" "&toPlace=50.1132737,8.6767235" "&time=2019-05-01T03:30Z" "&arriveBy=true" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=03:01, end=03:29, duration=00:29, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_FFM_101 [track=101, scheduled_track=101, level=-3], start=2019-05-01 03:01, mode="WALK", trip="-", end=2019-05-01 03:15), (from=test_FFM_101 [track=101, scheduled_track=101, level=-3], to=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], start=2019-05-01 03:15, mode="METRO", trip="S3", end=2019-05-01 03:20), (from=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 03:20, mode="WALK", trip="-", end=2019-05-01 03:29) ])", to_str(res.itineraries_)); } // Blocked near toPlace, arriveBy=true, must pass before blocked { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=50.106420,8.660708,-3" "&time=2019-05-01T02:54Z" "&arriveBy=true" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=01:34, end=02:40, duration=01:20, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 01:34, mode="WALK", trip="-", end=2019-05-01 01:35), (from=test_DA_10 [track=10, scheduled_track=10, level=-1, alerts=["Yeah"]], to=test_FFM_12 [track=12, scheduled_track=10, level=0], start=2019-05-01 01:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 01:55, alerts=["Hello"]), (from=test_FFM_12 [track=12, scheduled_track=10, level=0], to=- [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:30, mode="WALK", trip="-", end=2019-05-01 02:40) ])", to_str(res.itineraries_)); } // Blocked near toPlace, arriveBy=true, can pass after blocked { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=50.106420,8.660708,-3" "&time=2019-05-01T02:55Z" "&arriveBy=true" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=02:34, end=02:55, duration=00:21, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 02:34, mode="WALK", trip="-", end=2019-05-01 02:35), (from=test_DA_10 [track=10, scheduled_track=10, level=-1], to=test_FFM_10 [track=10, scheduled_track=10, level=0], start=2019-05-01 02:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 02:45), (from=test_FFM_10 [track=10, scheduled_track=10, level=0], to=- [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:45, mode="WALK", trip="-", end=2019-05-01 02:55) ])", to_str(res.itineraries_)); } // Blocked near toPlace, arriveBy=false, temporary blocked / must wait { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=50.106420,8.660708,-3" "&time=2019-05-01T01:30Z" "&arriveBy=false" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match 'toPlace' closely "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=01:34, end=02:40, duration=01:10, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 01:34, mode="WALK", trip="-", end=2019-05-01 01:35), (from=test_DA_10 [track=10, scheduled_track=10, level=-1, alerts=["Yeah"]], to=test_FFM_12 [track=12, scheduled_track=10, level=0], start=2019-05-01 01:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 01:55, alerts=["Hello"]), (from=test_FFM_12 [track=12, scheduled_track=10, level=0], to=- [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:30, mode="WALK", trip="-", end=2019-05-01 02:40) ])", to_str(res.itineraries_)); } // Blocked near toPlace, arriveBy=false, can pass after blocked { auto const res = routing( "?fromPlace=49.87336,8.62926" "&toPlace=50.106420,8.660708,-3" "&time=2019-05-01T01:40Z" "&arriveBy=false" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match closely for wheelchair "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=02:34, end=02:55, duration=01:15, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 02:34, mode="WALK", trip="-", end=2019-05-01 02:35), (from=test_DA_10 [track=10, scheduled_track=10, level=-1], to=test_FFM_10 [track=10, scheduled_track=10, level=0], start=2019-05-01 02:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 02:45), (from=test_FFM_10 [track=10, scheduled_track=10, level=0], to=- [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:45, mode="WALK", trip="-", end=2019-05-01 02:55) ])", to_str(res.itineraries_)); } // Direct routing, arriveBy=false, pass before blocked { auto const res = routing( "?fromPlace=50.10411515,8.658776549999999" "&toPlace=50.106420,8.660708,-3" "&time=2019-05-01T01:15Z" "&arriveBy=false" "&preTransitModes=WALK" "&timetableView=false" "&pedestrianProfile=WHEELCHAIR" "&maxMatchingDistance=8" // Should match places closely "&useRoutedTransfers=true"); EXPECT_EQ( R"(date=2019-05-01, start=01:15, end=01:32, duration=00:17, transfers=0, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=- [track=-, scheduled_track=-, level=-3], start=2019-05-01 01:15, mode="WALK", trip="-", end=2019-05-01 01:32) ])", to_str(res.direct_)); } } // Route with wheelchair. { auto const res = routing( "?fromPlace=49.87263,8.63127" "&toPlace=50.11347,8.67664" "&time=2019-05-01T01:25Z" "&pedestrianProfile=WHEELCHAIR" "&useRoutedTransfers=true" "&timetableView=false"); EXPECT_EQ( R"(date=2019-05-01, start=01:29, end=02:29, duration=01:04, transfers=1, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 01:29, mode="WALK", trip="-", end=2019-05-01 01:35), (from=test_DA_10 [track=10, scheduled_track=10, level=-1, alerts=["Yeah"]], to=test_FFM_12 [track=12, scheduled_track=10, level=0], start=2019-05-01 01:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 01:55, alerts=["Hello"]), (from=test_FFM_12 [track=12, scheduled_track=10, level=0], to=test_FFM_101 [track=101, scheduled_track=101, level=-3], start=2019-05-01 01:55, mode="WALK", trip="-", end=2019-05-01 02:02), (from=test_FFM_101 [track=101, scheduled_track=101, level=-3], to=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], start=2019-05-01 02:15, mode="METRO", trip="S3", end=2019-05-01 02:20), (from=test_FFM_HAUPT_S [track=-, scheduled_track=-, level=-3], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 02:20, mode="WALK", trip="-", end=2019-05-01 02:29) ])", to_str(res.itineraries_)); } // Route without wheelchair. { auto const res = routing( "?fromPlace=49.87263,8.63127" "&toPlace=50.11347,8.67664" "&time=2019-05-01T01:25Z" "&useRoutedTransfers=true" "&timetableView=false"); EXPECT_EQ( R"(date=2019-05-01, start=01:25, end=02:15, duration=00:50, transfers=1, legs=[ (from=- [track=-, scheduled_track=-, level=0], to=test_DA_10 [track=10, scheduled_track=10, level=-1], start=2019-05-01 01:25, mode="WALK", trip="-", end=2019-05-01 01:29), (from=test_DA_10 [track=10, scheduled_track=10, level=-1, alerts=["Yeah"]], to=test_FFM_12 [track=12, scheduled_track=10, level=0], start=2019-05-01 01:35, mode="HIGHSPEED_RAIL", trip="ICE", end=2019-05-01 01:55, alerts=["Hello"]), (from=test_FFM_12 [track=12, scheduled_track=10, level=0], to=test_de:6412:10:6:1 [track=U4, scheduled_track=U4, level=-2], start=2019-05-01 01:55, mode="WALK", trip="-", end=2019-05-01 02:00), (from=test_de:6412:10:6:1 [track=U4, scheduled_track=U4, level=-2], to=test_FFM_HAUPT_U [track=-, scheduled_track=-, level=-4], start=2019-05-01 02:05, mode="SUBWAY", trip="U4", end=2019-05-01 02:10), (from=test_FFM_HAUPT_U [track=-, scheduled_track=-, level=-4], to=- [track=-, scheduled_track=-, level=0], start=2019-05-01 02:10, mode="WALK", trip="-", end=2019-05-01 02:15) ])", to_str(res.itineraries_)); } // Route using radius: finds stops within 5km radius from coordinates, // no preTransitModes / OSM street routing needed. { // fromPlace is ~2km from DA_10, toPlace is ~2km from FFM_10. auto const res = routing( "?fromPlace=49.89100,8.62900" "&toPlace=50.08800,8.66100" "&time=2019-05-01T01:30Z" "&radius=5000" "&timetableView=false"); ASSERT_FALSE(res.itineraries_.empty()); EXPECT_TRUE(utl::any_of(res.itineraries_.front().legs_, [](auto const& l) { return l.routeShortName_.has_value() && *l.routeShortName_ == "ICE"; })); } // Route using radius on origin coordinate, station ID as destination. { // fromPlace is ~2km from DA_10, toPlace is the FFM_10 stop directly. auto const res = routing( "?fromPlace=49.89100,8.62900" "&toPlace=test_FFM_10" "&time=2019-05-01T01:30Z" "&radius=5000" "&timetableView=false"); ASSERT_FALSE(res.itineraries_.empty()); EXPECT_TRUE(utl::any_of(res.itineraries_.front().legs_, [](auto const& l) { return l.routeShortName_.has_value() && *l.routeShortName_ == "ICE"; })); } } ================================================ FILE: test/tag_lookup_test.cc ================================================ #include "gtest/gtest.h" #include "boost/url/url.hpp" #include "nigiri/types.h" #include "motis/config.h" #include "motis/data.h" #include "motis/import.h" #include "motis/tag_lookup.h" using namespace std::string_view_literals; using namespace osr; constexpr auto const kGTFS = R"( # agency.txt agency_id,agency_name,agency_url,agency_timezone DB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin # stops.txt stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code DA,DA Hbf,49.87260,8.63085,1,, DA_3,DA Hbf,49.87355,8.63003,0,DA,3 DA 10,DA Hbf,49.87336,8.62926,0,DA,10 FFM,FFM Hbf,50.10701,8.66341,1,, FFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101 FFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10 FFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12 de:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4 LANGEN,Langen,49.99359,8.65677,1,,1 FFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,, +FFM_HÄUPT_&U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT, FFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT, # routes.txt route_id,agency_id,route_short_name,route_long_name,route_desc,route_type S3 ,DB,S3,,,109 Ü4,DB,U4,,,402 +ICE_&A,DB,ICE,,,101 # trips.txt route_id,service_id,trip_id,trip_headsign,block_id S3 ,S1,S3 ,, Ü4,S1,Ü4,, +ICE_&A,S1,+ICE_&A,, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type S3 ,01:15:00,01:15:00,FFM_101,1,0,0 S3 ,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0 Ü4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0 Ü4,01:10:00,01:10:00,+FFM_HÄUPT_&U,1,0,0 +ICE_&A,00:35:00,00:35:00,DA 10,0,0,0 +ICE_&A,00:45:00,00:45:00,FFM_10,1,0,0 # calendar_dates.txt service_id,date,exception_type S1,20190501,1 )"sv; TEST(motis, tag_lookup) { auto ec = std::error_code{}; std::filesystem::remove_all("test/data", ec); auto const c = motis::config{ .server_ = {{.web_folder_ = "ui/build", .n_threads_ = 1U}}, .osm_ = {"test/resources/test_case.osm.pbf"}, .timetable_ = motis::config::timetable{ .first_day_ = "2019-05-01", .num_days_ = 2, .datasets_ = {{"test", {.path_ = std::string{kGTFS}}}}}}; motis::import(c, "test/data"); auto d = motis::data{"test/data", c}; auto const rt = d.rt_; auto const rtt = rt->rtt_.get(); EXPECT_TRUE( d.tags_->get_trip(*d.tt_, rtt, "20190501_01:15_test_S3 ").first.valid()); EXPECT_TRUE( d.tags_->get_trip(*d.tt_, rtt, "20190501_01:05_test_Ü4").first.valid()); EXPECT_TRUE(d.tags_->get_trip(*d.tt_, rtt, "20190501_00:35_test_+ICE_&A") .first.valid()); EXPECT_NE(nigiri::location_idx_t::invalid(), d.tags_->get_location(*d.tt_, "test_DA 10")); EXPECT_NE(nigiri::location_idx_t::invalid(), d.tags_->get_location(*d.tt_, "test_+FFM_HÄUPT_&U")); EXPECT_NE(nigiri::location_idx_t::invalid(), d.tags_->get_location(*d.tt_, "test_DA 10")); auto u = boost::urls::url{ "/api", }; u.params({true, false, false}).append({"encoded", "a+& b"}); u.params({false, false, false}).append({"unencoded", "a+& b"}); { std::stringstream buffer; buffer << u; EXPECT_EQ("/api?encoded=a%2B%26+b&unencoded=a+%26%20b", buffer.str()); } { std::stringstream buffer; buffer << u.encoded_params(); EXPECT_EQ("encoded=a%2B%26+b&unencoded=a+%26%20b", buffer.str()); } } ================================================ FILE: test/test_case/.gitignore ================================================ /*_data/ ================================================ FILE: test/test_case.cc ================================================ #include "./test_case.h" #include "motis/import.h" using motis::import; test_case_params const import_test_case(config const&& c, std::string_view path) { auto ec = std::error_code{}; std::filesystem::remove_all(path, ec); import(c, path); return {path, std::move(c)}; } ================================================ FILE: test/test_case.h ================================================ #pragma once /** * Framework for reusable tests * * Test cases need to be defined once and can then be used for multiple tests * See comments on how to add new test cases * * Reminder: Use with care! Try to reuse already existing tests first */ #include #include "motis/config.h" #include "motis/data.h" using motis::config; using motis::data; // Requires an element for each reusable test case enum class test_case { FFM_one_to_many, }; using test_case_params = std::pair; // Requires a specialisation for each test case template test_case_params const import_test_case(); // Most tests will only use 'data', but some might require access to 'config' template std::pair get_test_case() { static auto const params{import_test_case()}; return {data{std::get<0>(params), std::get<1>(params)}, std::get<1>(params)}; } test_case_params const import_test_case(config const&&, std::string_view path); ================================================ FILE: test/test_dir.h.in ================================================ #pragma once #define OSR_TEST_EXECUTION_DIR "@CMAKE_CURRENT_SOURCE_DIR@" ================================================ FILE: test/util.cc ================================================ #include "./util.h" #include "fmt/format.h" namespace motis::test { using namespace std::string_view_literals; using namespace std::chrono_literals; using namespace date; using feed_entity = std::variant; transit_realtime::FeedMessage to_feed_msg( std::vector const& feed_entities, date::sys_seconds const msg_time) { transit_realtime::FeedMessage msg; auto const hdr = msg.mutable_header(); hdr->set_gtfs_realtime_version("2.0"); hdr->set_incrementality( transit_realtime::FeedHeader_Incrementality_FULL_DATASET); hdr->set_timestamp(to_unix(msg_time)); auto id = 0U; for (auto const& x : feed_entities) { auto const e = msg.add_entity(); e->set_id(fmt::format("{}", ++id)); auto const set_trip = [](::transit_realtime::TripDescriptor* td, trip_descriptor const& trip, bool const canceled = false) { td->set_trip_id(trip.trip_id_); if (canceled) { td->set_schedule_relationship( transit_realtime::TripDescriptor_ScheduleRelationship_CANCELED); return; } if (trip.date_) { td->set_start_date(*trip.date_); } if (trip.start_time_) { td->set_start_time(*trip.start_time_); } }; std::visit( utl::overloaded{ [&](trip_update const& u) { set_trip(e->mutable_trip_update()->mutable_trip(), u.trip_, u.cancelled_); for (auto const& stop_upd : u.stop_updates_) { auto* const upd = e->mutable_trip_update()->add_stop_time_update(); if (!stop_upd.stop_id_.empty()) { *upd->mutable_stop_id() = stop_upd.stop_id_; } if (stop_upd.seq_.has_value()) { upd->set_stop_sequence(*stop_upd.seq_); } if (stop_upd.stop_assignment_.has_value()) { upd->mutable_stop_time_properties()->set_assigned_stop_id( stop_upd.stop_assignment_.value()); } if (stop_upd.skip_) { upd->set_schedule_relationship( transit_realtime:: TripUpdate_StopTimeUpdate_ScheduleRelationship_SKIPPED); continue; } stop_upd.ev_type_ == ::nigiri::event_type::kDep ? upd->mutable_departure()->set_delay( stop_upd.delay_minutes_ * 60) : upd->mutable_arrival()->set_delay( stop_upd.delay_minutes_ * 60); } }, [&](alert const& a) { auto const alert = e->mutable_alert(); auto const header = alert->mutable_header_text()->add_translation(); *header->mutable_text() = a.header_; *header->mutable_language() = "en"; auto const description = alert->mutable_description_text()->add_translation(); *description->mutable_text() = a.description_; *description->mutable_language() = "en"; for (auto const& entity : a.entities_) { auto const ie = alert->add_informed_entity(); if (entity.agency_id_) { *ie->mutable_agency_id() = *entity.agency_id_; } if (entity.route_id_) { *ie->mutable_route_id() = *entity.route_id_; } if (entity.direction_id_) { ie->set_direction_id(*entity.direction_id_); } if (entity.route_type_) { ie->set_route_type(*entity.route_type_); } if (entity.stop_id_) { ie->set_stop_id(*entity.stop_id_); } if (entity.trip_) { set_trip(ie->mutable_trip(), *entity.trip_); } } }}, x); } return msg; } } // namespace motis::test ================================================ FILE: test/util.h ================================================ #include #include "date/date.h" #include "gtfsrt/gtfs-realtime.pb.h" #include "nigiri/types.h" namespace motis::test { using namespace std::string_view_literals; using namespace date; using namespace std::chrono_literals; struct trip_descriptor { std::string trip_id_; std::optional start_time_; std::optional date_; }; struct trip_update { struct stop_time_update { std::string stop_id_; std::optional seq_{std::nullopt}; ::nigiri::event_type ev_type_{::nigiri::event_type::kDep}; std::int32_t delay_minutes_{0U}; bool skip_{false}; std::optional stop_assignment_{std::nullopt}; }; trip_descriptor trip_; std::vector stop_updates_{}; bool cancelled_{false}; }; struct alert { struct entity_selector { std::optional agency_id_{}; std::optional route_id_{}; std::optional route_type_{}; std::optional direction_id_{}; std::optional trip_{}; std::optional stop_id_{}; }; std::string header_; std::string description_; std::vector entities_; }; using feed_entity = std::variant; template std::uint64_t to_unix(T&& x) { return static_cast( std::chrono::time_point_cast(x) .time_since_epoch() .count()); }; transit_realtime::FeedMessage to_feed_msg( std::vector const& feed_entities, date::sys_seconds const msg_time); } // namespace motis::test ================================================ FILE: tools/buildcache-clang-tidy.lua ================================================ -- match(.*cmake.*) ------------------------------------------------------------------------------- -- This is a re-implementation of the C++ class gcc_wrapper_t. ------------------------------------------------------------------------------- require_std("io") require_std("os") require_std("string") require_std("table") require_std("bcache") ------------------------------------------------------------------------------- -- Internal helper functions. ------------------------------------------------------------------------------- local function make_preprocessor_cmd (args, preprocessed_file) local preprocess_args = {} -- Drop arguments that we do not want/need. local drop_next_arg = false for i, arg in ipairs(args) do local drop_this_arg = drop_next_arg drop_next_arg = false if arg == "-c" then drop_this_arg = true elseif arg == "-o" then drop_this_arg = true drop_next_arg = true end if not drop_this_arg and i > 6 then table.insert(preprocess_args, arg) end end -- Append the required arguments for producing preprocessed output. table.insert(preprocess_args, "-E") table.insert(preprocess_args, "-P") table.insert(preprocess_args, "-o") table.insert(preprocess_args, preprocessed_file) return preprocess_args end local function is_source_file (path) local ext = bcache.get_extension(path):lower() return (ext == ".cpp") or (ext == ".cc") or (ext == ".cxx") or (ext == ".c") end ------------------------------------------------------------------------------- -- Wrapper interface implementation. ------------------------------------------------------------------------------- function get_capabilities () -- We can use hard links with GCC since it will never overwrite already -- existing files. return { "hard_links" } end function preprocess_source () local expected = {"-E", "__run_co_compile", "--tidy", "--source", "--"} for i, arg in ipairs(expected) do if string.sub(ARGS[i+1], 1, #expected[i]) ~= expected[i] then error("Unexpected argument " .. i .. ": " .. ARGS[i+1] .. " -> '" .. string.sub(ARGS[i+1], 1, 1 + #expected[i]) .. "' != '" .. expected[i] .. "'") end end -- Check if this is a compilation command that we support. local is_object_compilation = false local has_object_output = false for i, arg in ipairs(ARGS) do if arg == "-c" then is_object_compilation = true elseif arg == "-o" then has_object_output = true elseif arg:sub(1, 1) == "@" then error("Response files are currently not supported.") end end if (not is_object_compilation) or (not has_object_output) then error("Unsupported complation command.") end -- Run the preprocessor step. local preprocessed_file = os.tmpname() local preprocessor_args = make_preprocessor_cmd(ARGS, preprocessed_file) local result = bcache.run(preprocessor_args) if result.return_code ~= 0 then os.remove(preprocessed_file) error("Preprocessing command was unsuccessful.") end -- Read and return the preprocessed file. local f = assert(io.open(preprocessed_file, "rb")) local preprocessed_source = f:read("*all") f:close() os.remove(preprocessed_file) return preprocessed_source end function get_relevant_arguments () local filtered_args = {} -- The first argument is the compiler binary without the path. table.insert(filtered_args, bcache.get_file_part(ARGS[1])) -- Note: We always skip the first arg since we have handled it already. local skip_next_arg = true for i, arg in ipairs(ARGS) do if not skip_next_arg then -- Does this argument specify a file (we don't want to hash those). local is_arg_plus_file_name = (arg == "-I") or (arg == "-MF") or (arg == "-MT") or (arg == "-MQ") or (arg == "-o") -- Generally unwanted argument (things that will not change how we go -- from preprocessed code to binary object files)? local first_two_chars = arg:sub(1, 2) local is_unwanted_arg = (first_two_chars == "-I") or (first_two_chars == "-D") or (first_two_chars == "-M") or is_source_file(arg) if is_arg_plus_file_name then skip_next_arg = true elseif not is_unwanted_arg then table.insert(filtered_args, arg) end else skip_next_arg = false end end return filtered_args end function get_program_id () -- TODO(m): Add things like executable file size too. -- Get the version string for the compiler. local result = bcache.run({ARGS[1], "--version"}) if result.return_code ~= 0 then error("Unable to get the compiler version information string.") end return result.std_out end function get_build_files () local files = {} local found_object_file = false for i = 2, #ARGS do local next_idx = i + 1 if (ARGS[i] == "-o") and (next_idx <= #ARGS) then if found_object_file then error("Only a single target object file can be specified.") end files["object"] = ARGS[next_idx] found_object_file = true elseif (ARGS[i] == "-ftest-coverage") then error("Code coverage data is currently not supported.") end end if not found_object_file then error("Unable to get the target object file.") end return files end ================================================ FILE: tools/suppress.txt ================================================ { uninitialized_values_written_to_memory_map Memcheck:Param msync(start) fun:msync fun:_ZN5cista4mmap4syncEv } { lmdb_uninitialized_write_1 Memcheck:Param pwrite64(buf) fun:__libc_pwrite64 fun:pwrite fun:mdb_page_flush fun:mdb_txn_commit } { lmdb_uninitialized_write_2 Memcheck:Param writev(vector[0]) fun:__writev fun:writev fun:mdb_page_flush fun:mdb_txn_commit } ================================================ FILE: tools/try-reproduce.py ================================================ #!/usr/bin/python3 import os import sys import subprocess import shutil import yaml from multiprocessing import Pool from pathlib import Path QUERIES = { 'raptor': { 'params': '?algorithm=RAPTOR&numItineraries=5&maxItineraries=5', 'exec': '/home/felix/code/motis/cmake-build-relwithdebinfo/motis' }, 'pong': { 'params': '?algorithm=PONG&numItineraries=5&maxItineraries=5', 'exec': '/home/felix/code/motis/cmake-build-relwithdebinfo/motis' } } def update_timetable_config(path): with open(path, 'r') as file: config = yaml.safe_load(file) # config['timetable']['tb'] = True config['timetable']['first_day'] = '2025-10-04' with open(path, 'w') as file: yaml.safe_dump(config, file) def cmd(cmd, cwd=None, verbose=False): try: if verbose: print(f"Running: {' '.join(cmd)}") if cwd: print(f"Working directory: {cwd}") result = subprocess.run(cmd, cwd=cwd, check=True, capture_output=True, text=True) if verbose: if result.stdout: print("STDOUT:", result.stdout) if result.stderr: print("STDERR:", result.stderr) return result.returncode except subprocess.CalledProcessError as e: print(f"Error running command: {' '.join(cmd)} [CWD={cwd}]") print(f"Return code: {e.returncode}") if e.stdout: print(f"STDOUT: {e.stdout}") if e.stderr: print(f"STDERR: {e.stderr}") def get_directories(path): """Get all directories in the given path.""" try: return [d for d in os.listdir(path) if d != 'data' and os.path.isdir(os.path.join(path, d))] except FileNotFoundError: print(f"Directory {path} not found") return [] def try_reproduce(id_value, verbose=False): motis = next(iter(QUERIES.items()))[1]['exec'] dir = f"reproduce/{id_value}" os.makedirs(dir, exist_ok=True) # Step 1: Execute motis extract extract_cmd = [ motis, "extract", "--filter_stops", "false", "-i", f"fail/{id_value}_0.json" ] # Add second JSON file only if it exists second_json = f"fail/{id_value}_1.json" if os.path.exists(second_json): extract_cmd.append(second_json) extract_cmd.extend(["-o", dir, '--reduce', 'true']) cmd(extract_cmd, verbose=verbose) # Step 2: Get timetable directories and run motis import timetable_dirs = get_directories(dir) if not timetable_dirs: print(f"Warning: No timetable directories found in {id_value}") sys.exit(1) cmd([motis, 'config'] + timetable_dirs, cwd=dir, verbose=verbose) update_timetable_config(f"{dir}/config.yml") cmd([motis, 'import'], cwd=dir, verbose=verbose) # Step 3: Copy query file for name, run in QUERIES.items(): cmd([ motis, 'params', '-i', f"../../fail/{id_value}_q.txt", '-o', f"queries-{name}.txt", '-p', run['params'] ], cwd=dir, verbose=verbose) # Step 4: Run Queries for name, params in QUERIES.items(): cmd([ params['exec'], 'batch', '-q', f"queries-{name}.txt", '-r', f"responses-{name}.txt" ], cwd=dir, verbose=verbose) # Step 5: Compare compare_cmd = [ motis, 'compare', '-q', f"queries-{next(iter(QUERIES))}.txt", '-r' ] compare_cmd.extend([ f"responses-{name}.txt" for name, params in QUERIES.items() ]) if cmd(compare_cmd, cwd=dir, verbose=verbose) == 0: if verbose: print("NO DIFF") return False else: if verbose: print("REPRODUCED") return True if __name__ == "__main__": if len(sys.argv) == 2: id_value = sys.argv[1] try_reproduce(id_value, True) else: with Pool(processes=6) as pool: query_ids = [d.removesuffix('_q.txt') for d in os.listdir('fail') if d.endswith('_q.txt')] pool.map(try_reproduce, query_ids) ================================================ FILE: tools/ubsan-suppress.txt ================================================ src:*/LuaJIT/* ================================================ FILE: ui/.gitignore ================================================ .DS_Store node_modules /build .vscode /.svelte-kit /package .env .env.* !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* /api/dist ================================================ FILE: ui/.npmrc ================================================ engine-strict=true ================================================ FILE: ui/.prettierignore ================================================ # Ignore files for PNPM, NPM and YARN pnpm-lock.yaml package-lock.json yarn.lock api/** src/lib/components/** static/sprite*.json static/icons/**.svg .pnpm-store ================================================ FILE: ui/.prettierrc ================================================ { "useTabs": true, "singleQuote": true, "trailingComma": "none", "printWidth": 100, "plugins": ["prettier-plugin-svelte"], "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } ================================================ FILE: ui/README.md ================================================ Build UI (the `-r` is important to also update the OpenAPI client): ```bash pnpm -r build ``` Generate OpenAPI client (when openapi.yaml has been changed, included in `pnpm -r build`): ```bash pnpm update-api ``` To publish a new version to npmjs: ```bash cd src/lib/api pnpm build pnpm version patch --no-git-tag-version pnpm publish --access public ``` ================================================ FILE: ui/api/LICENSE ================================================ MIT License Copyright (c) 2024 MOTIS Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: ui/api/README.md ================================================ # motis-client Pre-generated JS client for [MOTIS](https://github.com/motis-project/motis) based on the [OpenAPI definition](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/motis-project/motis/refs/heads/master/openapi.yaml#tag/routing/operation/plan). See there for parameters, responses and changes between API versions depending on MOTIS versions (correlating to motis-client versions). For example: ```js const response = await stoptimes({ throwOnError: true, baseUrl: 'https://api.transitous.org', headers: { 'User-Agent': 'my-user-agent' }, query: { stopId: 'de-DELFI_de:06412:7010:1:3', n: 10, radius: 500 } }); console.log(response); ``` ================================================ FILE: ui/api/openapi/index.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts export * from './schemas.gen'; export * from './services.gen'; export * from './types.gen'; ================================================ FILE: ui/api/openapi/schemas.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts export const AlertCauseSchema = { description: 'Cause of this alert.', type: 'string', enum: ['UNKNOWN_CAUSE', 'OTHER_CAUSE', 'TECHNICAL_PROBLEM', 'STRIKE', 'DEMONSTRATION', 'ACCIDENT', 'HOLIDAY', 'WEATHER', 'MAINTENANCE', 'CONSTRUCTION', 'POLICE_ACTIVITY', 'MEDICAL_EMERGENCY'] } as const; export const AlertEffectSchema = { description: 'The effect of this problem on the affected entity.', type: 'string', enum: ['NO_SERVICE', 'REDUCED_SERVICE', 'SIGNIFICANT_DELAYS', 'DETOUR', 'ADDITIONAL_SERVICE', 'MODIFIED_SERVICE', 'OTHER_EFFECT', 'UNKNOWN_EFFECT', 'STOP_MOVED', 'NO_EFFECT', 'ACCESSIBILITY_ISSUE'] } as const; export const AlertSeverityLevelSchema = { description: 'The severity of the alert.', type: 'string', enum: ['UNKNOWN_SEVERITY', 'INFO', 'WARNING', 'SEVERE'] } as const; export const TimeRangeSchema = { description: `A time interval. The interval is considered active at time t if t is greater than or equal to the start time and less than the end time. `, type: 'object', required: ['start', 'end'], properties: { start: { description: `If missing, the interval starts at minus infinity. If a TimeRange is provided, either start or end must be provided - both fields cannot be empty. `, type: 'string', format: 'date-time' }, end: { description: `If missing, the interval ends at plus infinity. If a TimeRange is provided, either start or end must be provided - both fields cannot be empty. `, type: 'string', format: 'date-time' } } } as const; export const AlertSchema = { description: 'An alert, indicating some sort of incident in the public transit network.', type: 'object', required: ['headerText', 'descriptionText'], properties: { code: { type: 'string', description: 'Attribute or notice code (e.g. for HRDF or NeTEx)' }, communicationPeriod: { description: `Time when the alert should be shown to the user. If missing, the alert will be shown as long as it appears in the feed. If multiple ranges are given, the alert will be shown during all of them. `, type: 'array', items: { '$ref': '#/components/schemas/TimeRange' } }, impactPeriod: { description: 'Time when the services are affected by the disruption mentioned in the alert.', type: 'array', items: { '$ref': '#/components/schemas/TimeRange' } }, cause: { '$ref': '#/components/schemas/AlertCause' }, causeDetail: { type: 'string', description: `Description of the cause of the alert that allows for agency-specific language; more specific than the Cause. ` }, effect: { '$ref': '#/components/schemas/AlertEffect' }, effectDetail: { type: 'string', description: `Description of the effect of the alert that allows for agency-specific language; more specific than the Effect. ` }, url: { type: 'string', description: 'The URL which provides additional information about the alert.' }, headerText: { type: 'string', description: `Header for the alert. This plain-text string will be highlighted, for example in boldface. ` }, descriptionText: { type: 'string', description: `Description for the alert. This plain-text string will be formatted as the body of the alert (or shown on an explicit "expand" request by the user). The information in the description should add to the information of the header. ` }, ttsHeaderText: { type: 'string', description: `Text containing the alert's header to be used for text-to-speech implementations. This field is the text-to-speech version of header_text. It should contain the same information as headerText but formatted such that it can read as text-to-speech (for example, abbreviations removed, numbers spelled out, etc.) ` }, ttsDescriptionText: { type: 'string', description: `Text containing a description for the alert to be used for text-to-speech implementations. This field is the text-to-speech version of description_text. It should contain the same information as description_text but formatted such that it can be read as text-to-speech (for example, abbreviations removed, numbers spelled out, etc.) ` }, severityLevel: { description: 'Severity of the alert.', '$ref': '#/components/schemas/AlertSeverityLevel' }, imageUrl: { description: 'String containing an URL linking to an image.', type: 'string' }, imageMediaType: { description: `IANA media type as to specify the type of image to be displayed. The type must start with "image/" `, type: 'string' }, imageAlternativeText: { description: `Text describing the appearance of the linked image in the image field (e.g., in case the image can't be displayed or the user can't see the image for accessibility reasons). See the HTML spec for alt image text. `, type: 'string' } } } as const; export const DurationSchema = { description: 'Object containing duration if a path was found or none if no path was found', type: 'object', properties: { duration: { type: 'number', description: 'duration in seconds if a path was found, otherwise missing', minimum: 0 }, distance: { type: 'number', description: 'distance in meters if a path was found and distance computation was requested, otherwise missing', minimum: 0 } } } as const; export const ParetoSetEntrySchema = { description: 'Object containing a single element of a ParetoSet', type: 'object', required: ['duration', 'transfers'], properties: { duration: { type: 'number', description: `duration in seconds for the the best solution using \`transfer\` transfers Notice that the resolution is currently in minutes, because of implementation details `, minimum: 0 }, transfers: { description: `The minimal number of transfers required to arrive within \`duration\` seconds transfers=0: Direct transit connecion without any transfers transfers=1: Transit connection with 1 transfer `, type: 'integer', minimum: 0 } } } as const; export const ParetoSetSchema = { description: 'Pareto set of optimal transit solutions', type: 'array', items: { '$ref': '#/components/schemas/ParetoSetEntry' } } as const; export const OneToManyIntermodalResponseSchema = { description: 'Object containing the optimal street and transit durations for One-to-Many routing', type: 'object', properties: { street_durations: { description: `Fastest durations for street routing The order of the items corresponds to the order of the \`many\` locations If no street routed connection is found, the corresponding \`Duration\` will be empty `, type: 'array', items: { '$ref': '#/components/schemas/Duration' } }, transit_durations: { description: `Pareto optimal solutions The order of the items corresponds to the order of the \`many\` locations If no connection using transits is found, the corresponding \`ParetoSet\` will be empty `, type: 'array', items: { '$ref': '#/components/schemas/ParetoSet' } } } } as const; export const AreaSchema = { description: 'Administrative area', type: 'object', required: ['name', 'adminLevel', 'matched'], properties: { name: { type: 'string', description: 'Name of the area' }, adminLevel: { type: 'number', description: `[OpenStreetMap \`admin_level\`](https://wiki.openstreetmap.org/wiki/Key:admin_level) of the area ` }, matched: { type: 'boolean', description: 'Whether this area was matched by the input text' }, unique: { type: 'boolean', description: `Set for the first area after the \`default\` area that distinguishes areas if the match is ambiguous regarding (\`default\` area + place name / street [+ house number]). ` }, default: { type: 'boolean', description: 'Whether this area should be displayed as default area (area with admin level closest 7)' } } } as const; export const TokenSchema = { description: 'Matched token range (from index, length)', type: 'array', minItems: 2, maxItems: 2, items: { type: 'number' } } as const; export const LocationTypeSchema = { description: 'location type', type: 'string', enum: ['ADDRESS', 'PLACE', 'STOP'] } as const; export const ModeSchema = { description: `# Street modes - \`WALK\` - \`BIKE\` - \`RENTAL\` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs. - \`CAR\` - \`CAR_PARKING\` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs. - \`CAR_DROPOFF\` Experimental. Expect unannounced breaking changes (without version bumps) for all perameters and returned structs. - \`ODM\` on-demand taxis from the Prima+ÖV Project - \`RIDE_SHARING\` ride sharing from the Prima+ÖV Project - \`FLEX\` flexible transports # Transit modes - \`TRANSIT\`: translates to \`TRAM,FERRY,AIRPLANE,BUS,COACH,RAIL,ODM,FUNICULAR,AERIAL_LIFT,OTHER\` - \`TRAM\`: trams - \`SUBWAY\`: subway trains (Paris Metro, London Underground, but also NYC Subway, Hamburger Hochbahn, and other non-underground services) - \`FERRY\`: ferries - \`AIRPLANE\`: airline flights - \`BUS\`: short distance buses (does not include \`COACH\`) - \`COACH\`: long distance buses (does not include \`BUS\`) - \`RAIL\`: translates to \`HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN,SUBWAY\` - \`HIGHSPEED_RAIL\`: long distance high speed trains (e.g. TGV) - \`LONG_DISTANCE\`: long distance inter city trains - \`NIGHT_RAIL\`: long distance night trains - \`REGIONAL_FAST_RAIL\`: deprecated, \`REGIONAL_RAIL\` will be used - \`REGIONAL_RAIL\`: regional train - \`SUBURBAN\`: suburban trains (e.g. S-Bahn, RER, Elizabeth Line, ...) - \`ODM\`: demand responsive transport - \`FUNICULAR\`: Funicular. Any rail system designed for steep inclines. - \`AERIAL_LIFT\`: Aerial lift, suspended cable car (e.g., gondola lift, aerial tramway). Cable transport where cabins, cars, gondolas or open chairs are suspended by means of one or more cables. - \`AREAL_LIFT\`: deprecated - \`METRO\`: deprecated - \`CABLE_CAR\`: deprecated `, type: 'string', enum: ['WALK', 'BIKE', 'RENTAL', 'CAR', 'CAR_PARKING', 'CAR_DROPOFF', 'ODM', 'RIDE_SHARING', 'FLEX', 'DEBUG_BUS_ROUTE', 'DEBUG_RAILWAY_ROUTE', 'DEBUG_FERRY_ROUTE', 'TRANSIT', 'TRAM', 'SUBWAY', 'FERRY', 'AIRPLANE', 'BUS', 'COACH', 'RAIL', 'HIGHSPEED_RAIL', 'LONG_DISTANCE', 'NIGHT_RAIL', 'REGIONAL_FAST_RAIL', 'REGIONAL_RAIL', 'SUBURBAN', 'FUNICULAR', 'AERIAL_LIFT', 'OTHER', 'AREAL_LIFT', 'METRO', 'CABLE_CAR'] } as const; export const MatchSchema = { description: 'GeoCoding match', type: 'object', required: ['type', 'name', 'id', 'lat', 'lon', 'tokens', 'areas', 'score'], properties: { type: { '$ref': '#/components/schemas/LocationType' }, category: { description: `Experimental. Type categories might be adjusted. For OSM stop locations: the amenity type based on https://wiki.openstreetmap.org/wiki/OpenStreetMap_Carto/Symbols `, type: 'string' }, tokens: { description: 'list of non-overlapping tokens that were matched', type: 'array', items: { '$ref': '#/components/schemas/Token' } }, name: { description: 'name of the location (transit stop / PoI / address)', type: 'string' }, id: { description: 'unique ID of the location', type: 'string' }, lat: { description: 'latitude', type: 'number' }, lon: { description: 'longitude', type: 'number' }, level: { description: `level according to OpenStreetMap (at the moment only for public transport) `, type: 'number' }, street: { description: 'street name', type: 'string' }, houseNumber: { description: 'house number', type: 'string' }, country: { description: 'ISO3166-1 country code from OpenStreetMap', type: 'string' }, zip: { description: 'zip code', type: 'string' }, tz: { description: 'timezone name (e.g. "Europe/Berlin")', type: 'string' }, areas: { description: 'list of areas', type: 'array', items: { '$ref': '#/components/schemas/Area' } }, score: { description: 'score according to the internal scoring system (the scoring algorithm might change in the future)', type: 'number' }, modes: { description: 'available transport modes for stops', type: 'array', items: { '$ref': '#/components/schemas/Mode' } }, importance: { description: 'importance of a stop, normalized from [0, 1]', type: 'number' } } } as const; export const ElevationCostsSchema = { description: `Different elevation cost profiles for street routing. Using a elevation cost profile will prefer routes with a smaller incline and smaller difference in elevation, even if the routed way is longer. - \`NONE\`: Ignore elevation data for routing. This is the default behavior - \`LOW\`: Add a low penalty for inclines. This will favor longer paths, if the elevation increase and incline are smaller. - \`HIGH\`: Add a high penalty for inclines. This will favor even longer paths, if the elevation increase and incline are smaller. `, type: 'string', enum: ['NONE', 'LOW', 'HIGH'] } as const; export const PedestrianProfileSchema = { description: 'Different accessibility profiles for pedestrians.', type: 'string', enum: ['FOOT', 'WHEELCHAIR'] } as const; export const PedestrianSpeedSchema = { description: 'Average speed for pedestrian routing in meters per second', type: 'number' } as const; export const CyclingSpeedSchema = { description: 'Average speed for bike routing in meters per second', type: 'number' } as const; export const VertexTypeSchema = { type: 'string', description: `- \`NORMAL\` - latitude / longitude coordinate or address - \`BIKESHARE\` - bike sharing station - \`TRANSIT\` - transit stop `, enum: ['NORMAL', 'BIKESHARE', 'TRANSIT'] } as const; export const PickupDropoffTypeSchema = { type: 'string', description: `- \`NORMAL\` - entry/exit is possible normally - \`NOT_ALLOWED\` - entry/exit is not allowed `, enum: ['NORMAL', 'NOT_ALLOWED'] } as const; export const PlaceSchema = { type: 'object', required: ['name', 'lat', 'lon', 'level'], properties: { name: { description: 'name of the transit stop / PoI / address', type: 'string' }, stopId: { description: "The ID of the stop. This is often something that users don't care about.", type: 'string' }, parentId: { description: "If it's not a root stop, this field contains the `stopId` of the parent stop.", type: 'string' }, importance: { description: 'The importance of the stop between 0-1.', type: 'number' }, lat: { description: 'latitude', type: 'number' }, lon: { description: 'longitude', type: 'number' }, level: { description: 'level according to OpenStreetMap', type: 'number' }, tz: { description: 'timezone name (e.g. "Europe/Berlin")', type: 'string' }, arrival: { description: 'arrival time', type: 'string', format: 'date-time' }, departure: { description: 'departure time', type: 'string', format: 'date-time' }, scheduledArrival: { description: 'scheduled arrival time', type: 'string', format: 'date-time' }, scheduledDeparture: { description: 'scheduled departure time', type: 'string', format: 'date-time' }, scheduledTrack: { description: 'scheduled track from the static schedule timetable dataset', type: 'string' }, track: { description: `The current track/platform information, updated with real-time updates if available. Can be missing if neither real-time updates nor the schedule timetable contains track information. `, type: 'string' }, description: { description: 'description of the location that provides more detailed information', type: 'string' }, vertexType: { '$ref': '#/components/schemas/VertexType' }, pickupType: { description: 'Type of pickup. It could be disallowed due to schedule, skipped stops or cancellations.', '$ref': '#/components/schemas/PickupDropoffType' }, dropoffType: { description: 'Type of dropoff. It could be disallowed due to schedule, skipped stops or cancellations.', '$ref': '#/components/schemas/PickupDropoffType' }, cancelled: { description: 'Whether this stop is cancelled due to the realtime situation.', type: 'boolean' }, alerts: { description: 'Alerts for this stop.', type: 'array', items: { '$ref': '#/components/schemas/Alert' } }, flex: { description: 'for `FLEX` transports, the flex location area or location group name', type: 'string' }, flexId: { description: 'for `FLEX` transports, the flex location area ID or location group ID', type: 'string' }, flexStartPickupDropOffWindow: { description: 'Time that on-demand service becomes available', type: 'string', format: 'date-time' }, flexEndPickupDropOffWindow: { description: 'Time that on-demand service ends', type: 'string', format: 'date-time' }, modes: { description: 'available transport modes for stops', type: 'array', items: { '$ref': '#/components/schemas/Mode' } } } } as const; export const ReachablePlaceSchema = { description: 'Place reachable by One-to-All', type: 'object', properties: { place: { '$ref': '#/components/schemas/Place', description: 'Place reached by One-to-All' }, duration: { type: 'integer', description: 'Total travel duration' }, k: { type: 'integer', description: `k is the smallest number, for which a journey with the shortest duration and at most k-1 transfers exist. You can think of k as the number of connections used. In more detail: k=0: No connection, e.g. for the one location k=1: Direct connection k=2: Connection with 1 transfer ` } } } as const; export const ReachableSchema = { description: 'Object containing all reachable places by One-to-All search', type: 'object', properties: { one: { '$ref': '#/components/schemas/Place', description: 'One location used in One-to-All search' }, all: { description: 'List of locations reachable by One-to-All', type: 'array', items: { '$ref': '#/components/schemas/ReachablePlace' } } } } as const; export const StopTimeSchema = { description: 'departure or arrival event at a stop', type: 'object', required: ['place', 'mode', 'realTime', 'headsign', 'tripFrom', 'tripTo', 'agencyId', 'agencyName', 'agencyUrl', 'tripId', 'routeId', 'directionId', 'routeShortName', 'routeLongName', 'tripShortName', 'displayName', 'pickupDropoffType', 'cancelled', 'tripCancelled', 'source'], properties: { place: { '$ref': '#/components/schemas/Place', description: 'information about the stop place and time' }, mode: { '$ref': '#/components/schemas/Mode', description: 'Transport mode for this leg' }, realTime: { description: 'Whether there is real-time data about this leg', type: 'boolean' }, headsign: { description: `The headsign of the bus or train being used. For non-transit legs, null `, type: 'string' }, tripFrom: { description: 'first stop of this trip', '$ref': '#/components/schemas/Place' }, tripTo: { description: 'final stop of this trip', '$ref': '#/components/schemas/Place' }, agencyId: { type: 'string' }, agencyName: { type: 'string' }, agencyUrl: { type: 'string' }, routeId: { type: 'string' }, routeUrl: { type: 'string' }, directionId: { type: 'string' }, routeColor: { type: 'string' }, routeTextColor: { type: 'string' }, tripId: { type: 'string' }, routeType: { type: 'integer' }, routeShortName: { type: 'string' }, routeLongName: { type: 'string' }, tripShortName: { type: 'string' }, displayName: { type: 'string' }, previousStops: { type: 'array', description: `Experimental. Expect unannounced breaking changes (without version bumps). Stops on the trips before this stop. Returned only if \`fetchStop\` and \`arriveBy\` are \`true\`. `, items: { '$ref': '#/components/schemas/Place' } }, nextStops: { type: 'array', description: `Experimental. Expect unannounced breaking changes (without version bumps). Stops on the trips after this stop. Returned only if \`fetchStop\` is \`true\` and \`arriveBy\` is \`false\`. `, items: { '$ref': '#/components/schemas/Place' } }, pickupDropoffType: { description: 'Type of pickup (for departures) or dropoff (for arrivals), may be disallowed either due to schedule, skipped stops or cancellations', '$ref': '#/components/schemas/PickupDropoffType' }, cancelled: { description: 'Whether the departure/arrival is cancelled due to the realtime situation (either because the stop is skipped or because the entire trip is cancelled).', type: 'boolean' }, tripCancelled: { description: 'Whether the entire trip is cancelled due to the realtime situation.', type: 'boolean' }, source: { description: 'Filename and line number where this trip is from', type: 'string' } } } as const; export const TripInfoSchema = { description: 'trip id and name', type: 'object', required: ['tripId'], properties: { tripId: { description: 'trip ID (dataset trip id prefixed with the dataset tag)', type: 'string' }, routeShortName: { description: 'trip display name (api version < 4)', type: 'string' }, displayName: { description: 'trip display name (api version >= 4)', type: 'string' } } } as const; export const TripSegmentSchema = { description: 'trip segment between two stops to show a trip on a map', type: 'object', required: ['trips', 'mode', 'distance', 'from', 'to', 'departure', 'arrival', 'scheduledArrival', 'scheduledDeparture', 'realTime', 'polyline'], properties: { trips: { type: 'array', items: { '$ref': '#/components/schemas/TripInfo' } }, routeColor: { type: 'string' }, mode: { '$ref': '#/components/schemas/Mode', description: 'Transport mode for this leg' }, distance: { type: 'number', description: 'distance in meters' }, from: { '$ref': '#/components/schemas/Place' }, to: { '$ref': '#/components/schemas/Place' }, departure: { description: 'departure time', type: 'string', format: 'date-time' }, arrival: { description: 'arrival time', type: 'string', format: 'date-time' }, scheduledDeparture: { description: 'scheduled departure time', type: 'string', format: 'date-time' }, scheduledArrival: { description: 'scheduled arrival time', type: 'string', format: 'date-time' }, realTime: { description: 'Whether there is real-time data about this leg', type: 'boolean' }, polyline: { description: 'Google polyline encoded coordinate sequence (with precision 5) where the trip travels on this segment.', type: 'string' } } } as const; export const DirectionSchema = { type: 'string', enum: ['DEPART', 'HARD_LEFT', 'LEFT', 'SLIGHTLY_LEFT', 'CONTINUE', 'SLIGHTLY_RIGHT', 'RIGHT', 'HARD_RIGHT', 'CIRCLE_CLOCKWISE', 'CIRCLE_COUNTERCLOCKWISE', 'STAIRS', 'ELEVATOR', 'UTURN_LEFT', 'UTURN_RIGHT'] } as const; export const EncodedPolylineSchema = { type: 'object', required: ['points', 'precision', 'length'], properties: { points: { description: 'The encoded points of the polyline using the Google polyline encoding.', type: 'string' }, precision: { description: `The precision of the returned polyline (7 for /v1, 6 for /v2) Be aware that with precision 7, coordinates with |longitude| > 107.37 are undefined/will overflow. `, type: 'integer' }, length: { description: 'The number of points in the string', type: 'integer', minimum: 0 } } } as const; export const StepInstructionSchema = { type: 'object', required: ['fromLevel', 'toLevel', 'polyline', 'relativeDirection', 'distance', 'streetName', 'exit', 'stayOn', 'area'], properties: { relativeDirection: { '$ref': '#/components/schemas/Direction' }, distance: { description: 'The distance in meters that this step takes.', type: 'number' }, fromLevel: { description: 'level where this segment starts, based on OpenStreetMap data', type: 'number' }, toLevel: { description: 'level where this segment starts, based on OpenStreetMap data', type: 'number' }, osmWay: { description: 'OpenStreetMap way index', type: 'integer' }, polyline: { '$ref': '#/components/schemas/EncodedPolyline' }, streetName: { description: 'The name of the street.', type: 'string' }, exit: { description: `Not implemented! When exiting a highway or traffic circle, the exit name/number. `, type: 'string' }, stayOn: { description: `Not implemented! Indicates whether or not a street changes direction at an intersection. `, type: 'boolean' }, area: { description: `Not implemented! This step is on an open area, such as a plaza or train platform, and thus the directions should say something like "cross" `, type: 'boolean' }, toll: { description: 'Indicates that a fee must be paid by general traffic to use a road, road bridge or road tunnel.', type: 'boolean' }, accessRestriction: { description: `Experimental. Indicates whether access to this part of the route is restricted. See: https://wiki.openstreetmap.org/wiki/Conditional_restrictions `, type: 'string' }, elevationUp: { type: 'integer', description: 'incline in meters across this path segment' }, elevationDown: { type: 'integer', description: 'decline in meters across this path segment' } } } as const; export const RentalFormFactorSchema = { type: 'string', enum: ['BICYCLE', 'CARGO_BICYCLE', 'CAR', 'MOPED', 'SCOOTER_STANDING', 'SCOOTER_SEATED', 'OTHER'] } as const; export const RentalPropulsionTypeSchema = { type: 'string', enum: ['HUMAN', 'ELECTRIC_ASSIST', 'ELECTRIC', 'COMBUSTION', 'COMBUSTION_DIESEL', 'HYBRID', 'PLUG_IN_HYBRID', 'HYDROGEN_FUEL_CELL'] } as const; export const RentalReturnConstraintSchema = { type: 'string', enum: ['NONE', 'ANY_STATION', 'ROUNDTRIP_STATION'] } as const; export const RentalSchema = { description: 'Vehicle rental', type: 'object', required: ['providerId', 'providerGroupId', 'systemId'], properties: { providerId: { type: 'string', description: 'Rental provider ID' }, providerGroupId: { type: 'string', description: 'Rental provider group ID' }, systemId: { type: 'string', description: 'Vehicle share system ID' }, systemName: { type: 'string', description: 'Vehicle share system name' }, url: { type: 'string', description: 'URL of the vehicle share system' }, color: { type: 'string', description: `Color associated with this provider, in hexadecimal RGB format (e.g. "#FF0000" for red). Can be empty. ` }, stationName: { type: 'string', description: 'Name of the station' }, fromStationName: { type: 'string', description: 'Name of the station where the vehicle is picked up (empty for free floating vehicles)' }, toStationName: { type: 'string', description: 'Name of the station where the vehicle is returned (empty for free floating vehicles)' }, rentalUriAndroid: { type: 'string', description: 'Rental URI for Android (deep link to the specific station or vehicle)' }, rentalUriIOS: { type: 'string', description: 'Rental URI for iOS (deep link to the specific station or vehicle)' }, rentalUriWeb: { type: 'string', description: 'Rental URI for web (deep link to the specific station or vehicle)' }, formFactor: { '$ref': '#/components/schemas/RentalFormFactor' }, propulsionType: { '$ref': '#/components/schemas/RentalPropulsionType' }, returnConstraint: { '$ref': '#/components/schemas/RentalReturnConstraint' } } } as const; export const MultiPolygonSchema = { type: 'array', description: `A multi-polygon contains a number of polygons, each containing a number of rings, which are encoded as polylines (with precision 6). For each polygon, the first ring is the outer ring, all subsequent rings are inner rings (holes). `, items: { type: 'array', items: { '$ref': '#/components/schemas/EncodedPolyline' } } } as const; export const RentalZoneRestrictionsSchema = { type: 'object', required: ['vehicleTypeIdxs', 'rideStartAllowed', 'rideEndAllowed', 'rideThroughAllowed'], properties: { vehicleTypeIdxs: { type: 'array', description: `List of vehicle types (as indices into the provider's vehicle types array) to which these restrictions apply. If empty, the restrictions apply to all vehicle types of the provider. `, items: { type: 'integer' } }, rideStartAllowed: { type: 'boolean', description: 'whether the ride is allowed to start in this zone' }, rideEndAllowed: { type: 'boolean', description: 'whether the ride is allowed to end in this zone' }, rideThroughAllowed: { type: 'boolean', description: 'whether the ride is allowed to pass through this zone' }, stationParking: { type: 'boolean', description: 'whether vehicles can only be parked at stations in this zone' } } } as const; export const RentalVehicleTypeSchema = { type: 'object', required: ['id', 'formFactor', 'propulsionType', 'returnConstraint', 'returnConstraintGuessed'], properties: { id: { type: 'string', description: 'Unique identifier of the vehicle type (unique within the provider)' }, name: { type: 'string', description: 'Public name of the vehicle type (can be empty)' }, formFactor: { '$ref': '#/components/schemas/RentalFormFactor' }, propulsionType: { '$ref': '#/components/schemas/RentalPropulsionType' }, returnConstraint: { '$ref': '#/components/schemas/RentalReturnConstraint' }, returnConstraintGuessed: { type: 'boolean', description: 'Whether the return constraint was guessed instead of being specified by the rental provider' } } } as const; export const RentalProviderSchema = { type: 'object', required: ['id', 'name', 'groupId', 'bbox', 'vehicleTypes', 'formFactors', 'defaultRestrictions', 'globalGeofencingRules'], properties: { id: { type: 'string', description: 'Unique identifier of the rental provider' }, name: { type: 'string', description: 'Name of the provider to be displayed to customers' }, groupId: { type: 'string', description: 'Id of the rental provider group this provider belongs to' }, operator: { type: 'string', description: 'Name of the system operator' }, url: { type: 'string', description: 'URL of the vehicle share system' }, purchaseUrl: { type: 'string', description: 'URL where a customer can purchase a membership' }, color: { type: 'string', description: `Color associated with this provider, in hexadecimal RGB format (e.g. "#FF0000" for red). Can be empty. ` }, bbox: { type: 'array', description: `Bounding box of the area covered by this rental provider, [west, south, east, north] as [lon, lat, lon, lat] `, minItems: 4, maxItems: 4, items: { type: 'number' } }, vehicleTypes: { type: 'array', items: { '$ref': '#/components/schemas/RentalVehicleType' } }, formFactors: { type: 'array', description: 'List of form factors offered by this provider', items: { '$ref': '#/components/schemas/RentalFormFactor' } }, defaultRestrictions: { '$ref': '#/components/schemas/RentalZoneRestrictions' }, globalGeofencingRules: { type: 'array', items: { '$ref': '#/components/schemas/RentalZoneRestrictions' } } } } as const; export const RentalProviderGroupSchema = { type: 'object', required: ['id', 'name', 'providers', 'formFactors'], properties: { id: { type: 'string', description: 'Unique identifier of the rental provider group' }, name: { type: 'string', description: 'Name of the provider group to be displayed to customers' }, color: { type: 'string', description: `Color associated with this provider group, in hexadecimal RGB format (e.g. "#FF0000" for red). Can be empty. ` }, providers: { type: 'array', description: 'List of rental provider IDs that belong to this group', items: { type: 'string' } }, formFactors: { type: 'array', description: 'List of form factors offered by this provider group', items: { '$ref': '#/components/schemas/RentalFormFactor' } } } } as const; export const RentalStationSchema = { type: 'object', required: ['id', 'providerId', 'providerGroupId', 'name', 'lat', 'lon', 'isRenting', 'isReturning', 'numVehiclesAvailable', 'formFactors', 'vehicleTypesAvailable', 'vehicleDocksAvailable', 'bbox'], properties: { id: { type: 'string', description: 'Unique identifier of the rental station' }, providerId: { type: 'string', description: 'Unique identifier of the rental provider' }, providerGroupId: { type: 'string', description: 'Unique identifier of the rental provider group' }, name: { type: 'string', description: 'Public name of the station' }, lat: { description: 'latitude', type: 'number' }, lon: { description: 'longitude', type: 'number' }, address: { type: 'string', description: 'Address where the station is located' }, crossStreet: { type: 'string', description: 'Cross street or landmark where the station is located' }, rentalUriAndroid: { type: 'string', description: 'Rental URI for Android (deep link to the specific station)' }, rentalUriIOS: { type: 'string', description: 'Rental URI for iOS (deep link to the specific station)' }, rentalUriWeb: { type: 'string', description: 'Rental URI for web (deep link to the specific station)' }, isRenting: { type: 'boolean', description: 'true if vehicles can be rented from this station, false if it is temporarily out of service' }, isReturning: { type: 'boolean', description: 'true if vehicles can be returned to this station, false if it is temporarily out of service' }, numVehiclesAvailable: { type: 'integer', description: 'Number of vehicles available for rental at this station' }, formFactors: { type: 'array', description: 'List of form factors available for rental and/or return at this station', items: { '$ref': '#/components/schemas/RentalFormFactor' } }, vehicleTypesAvailable: { type: 'object', description: 'List of vehicle types currently available at this station (vehicle type ID -> count)', additionalProperties: { type: 'integer' } }, vehicleDocksAvailable: { type: 'object', description: 'List of vehicle docks currently available at this station (vehicle type ID -> count)', additionalProperties: { type: 'integer' } }, stationArea: { '$ref': '#/components/schemas/MultiPolygon' }, bbox: { type: 'array', description: `Bounding box of the area covered by this station, [west, south, east, north] as [lon, lat, lon, lat] `, minItems: 4, maxItems: 4, items: { type: 'number' } } } } as const; export const RentalVehicleSchema = { type: 'object', required: ['id', 'providerId', 'providerGroupId', 'typeId', 'lat', 'lon', 'formFactor', 'propulsionType', 'returnConstraint', 'isReserved', 'isDisabled'], properties: { id: { type: 'string', description: 'Unique identifier of the rental vehicle' }, providerId: { type: 'string', description: 'Unique identifier of the rental provider' }, providerGroupId: { type: 'string', description: 'Unique identifier of the rental provider group' }, typeId: { type: 'string', description: 'Vehicle type ID (unique within the provider)' }, lat: { description: 'latitude', type: 'number' }, lon: { description: 'longitude', type: 'number' }, formFactor: { '$ref': '#/components/schemas/RentalFormFactor' }, propulsionType: { '$ref': '#/components/schemas/RentalPropulsionType' }, returnConstraint: { '$ref': '#/components/schemas/RentalReturnConstraint' }, stationId: { type: 'string', description: 'Station ID if the vehicle is currently at a station' }, homeStationId: { type: 'string', description: 'Station ID where the vehicle must be returned, if applicable' }, isReserved: { type: 'boolean', description: 'true if the vehicle is currently reserved by a customer, false otherwise' }, isDisabled: { type: 'boolean', description: 'true if the vehicle is out of service, false otherwise' }, rentalUriAndroid: { type: 'string', description: 'Rental URI for Android (deep link to the specific vehicle)' }, rentalUriIOS: { type: 'string', description: 'Rental URI for iOS (deep link to the specific vehicle)' }, rentalUriWeb: { type: 'string', description: 'Rental URI for web (deep link to the specific vehicle)' } } } as const; export const RentalZoneSchema = { type: 'object', required: ['providerId', 'providerGroupId', 'z', 'bbox', 'area', 'rules'], properties: { providerId: { type: 'string', description: 'Unique identifier of the rental provider' }, providerGroupId: { type: 'string', description: 'Unique identifier of the rental provider group' }, name: { type: 'string', description: 'Public name of the geofencing zone' }, z: { type: 'integer', description: 'Zone precedence / z-index (higher number = higher precedence)' }, bbox: { type: 'array', description: `Bounding box of the area covered by this zone, [west, south, east, north] as [lon, lat, lon, lat] `, minItems: 4, maxItems: 4, items: { type: 'number' } }, area: { '$ref': '#/components/schemas/MultiPolygon' }, rules: { type: 'array', items: { '$ref': '#/components/schemas/RentalZoneRestrictions' } } } } as const; export const CategorySchema = { type: 'object', required: ['id', 'name', 'shortName'], description: `not available for GTFS datasets by default For NeTEx it contains information about the vehicle category, e.g. IC/InterCity `, properties: { id: { type: 'string' }, name: { type: 'string' }, shortName: { type: 'string' } } } as const; export const LegSchema = { type: 'object', required: ['mode', 'startTime', 'endTime', 'scheduledStartTime', 'scheduledEndTime', 'realTime', 'scheduled', 'duration', 'from', 'to', 'legGeometry'], properties: { mode: { '$ref': '#/components/schemas/Mode', description: 'Transport mode for this leg' }, from: { '$ref': '#/components/schemas/Place' }, to: { '$ref': '#/components/schemas/Place' }, duration: { description: `Leg duration in seconds If leg is footpath: The footpath duration is derived from the default footpath duration using the query parameters \`transferTimeFactor\` and \`additionalTransferTime\` as follows: \`leg.duration = defaultDuration * transferTimeFactor + additionalTransferTime.\` In case the defaultDuration is needed, it can be calculated by \`defaultDuration = (leg.duration - additionalTransferTime) / transferTimeFactor\`. Note that the default values are \`transferTimeFactor = 1\` and \`additionalTransferTime = 0\` in case they are not explicitly provided in the query. `, type: 'integer' }, startTime: { type: 'string', format: 'date-time', description: 'leg departure time' }, endTime: { type: 'string', format: 'date-time', description: 'leg arrival time' }, scheduledStartTime: { type: 'string', format: 'date-time', description: 'scheduled leg departure time' }, scheduledEndTime: { type: 'string', format: 'date-time', description: 'scheduled leg arrival time' }, realTime: { description: 'Whether there is real-time data about this leg', type: 'boolean' }, scheduled: { description: `Whether this leg was originally scheduled to run or is an additional service. Scheduled times will equal realtime times in this case. `, type: 'boolean' }, distance: { description: 'For non-transit legs the distance traveled while traversing this leg in meters.', type: 'number' }, interlineWithPreviousLeg: { description: 'For transit legs, if the rider should stay on the vehicle as it changes route names.', type: 'boolean' }, headsign: { description: `For transit legs, the headsign of the bus or train being used. For non-transit legs, null `, type: 'string' }, tripFrom: { description: 'first stop of this trip', '$ref': '#/components/schemas/Place' }, tripTo: { description: 'final stop of this trip (can differ from headsign)', '$ref': '#/components/schemas/Place' }, category: { '$ref': '#/components/schemas/Category' }, routeId: { type: 'string' }, routeUrl: { type: 'string' }, directionId: { type: 'string' }, routeColor: { type: 'string' }, routeTextColor: { type: 'string' }, routeType: { type: 'integer' }, agencyName: { type: 'string' }, agencyUrl: { type: 'string' }, agencyId: { type: 'string' }, tripId: { type: 'string' }, routeShortName: { type: 'string' }, routeLongName: { type: 'string' }, tripShortName: { type: 'string' }, displayName: { type: 'string' }, cancelled: { description: 'Whether this trip is cancelled', type: 'boolean' }, source: { description: 'Filename and line number where this trip is from', type: 'string' }, intermediateStops: { description: `For transit legs, intermediate stops between the Place where the leg originates and the Place where the leg ends. For non-transit legs, null. `, type: 'array', items: { '$ref': '#/components/schemas/Place' } }, legGeometry: { description: `Encoded geometry of the leg. If detailed leg output is disabled, this is returned as an empty polyline. `, '$ref': '#/components/schemas/EncodedPolyline' }, steps: { description: `A series of turn by turn instructions used for walking, biking and driving. This field is omitted if the request disables detailed leg output. `, type: 'array', items: { '$ref': '#/components/schemas/StepInstruction' } }, rental: { '$ref': '#/components/schemas/Rental' }, fareTransferIndex: { type: 'integer', description: `Index into \`Itinerary.fareTransfers\` array to identify which fare transfer this leg belongs to ` }, effectiveFareLegIndex: { type: 'integer', description: `Index into the \`Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts\` array to identify which effective fare leg this itinerary leg belongs to ` }, alerts: { description: 'Alerts for this stop.', type: 'array', items: { '$ref': '#/components/schemas/Alert' } }, loopedCalendarSince: { description: `If set, this attribute indicates that this trip has been expanded beyond the feed end date (enabled by config flag \`timetable.dataset.extend_calendar\`) by looping active weekdays, e.g. from calendar.txt in GTFS. `, type: 'string', format: 'date-time' }, bikesAllowed: { description: `Whether bikes can be carried on this leg. `, type: 'boolean' } } } as const; export const RiderCategorySchema = { type: 'object', required: ['riderCategoryName', 'isDefaultFareCategory'], properties: { riderCategoryName: { description: 'Rider category name as displayed to the rider.', type: 'string' }, isDefaultFareCategory: { description: 'Specifies if this category should be considered the default (i.e. the main category displayed to riders).', type: 'boolean' }, eligibilityUrl: { description: 'URL to a web page providing detailed information about the rider category and/or its eligibility criteria.', type: 'string' } } } as const; export const FareMediaTypeSchema = { type: 'string', enum: ['NONE', 'PAPER_TICKET', 'TRANSIT_CARD', 'CONTACTLESS_EMV', 'MOBILE_APP'], description: `- \`NONE\`: No fare media involved (e.g., cash payment) - \`PAPER_TICKET\`: Physical paper ticket - \`TRANSIT_CARD\`: Physical transit card with stored value - \`CONTACTLESS_EMV\`: cEMV (contactless payment) - \`MOBILE_APP\`: Mobile app with virtual transit cards/passes ` } as const; export const FareMediaSchema = { type: 'object', required: ['fareMediaType'], properties: { fareMediaName: { description: 'Name of the fare media. Required for transit cards and mobile apps.', type: 'string' }, fareMediaType: { description: 'The type of fare media.', '$ref': '#/components/schemas/FareMediaType' } } } as const; export const FareProductSchema = { type: 'object', required: ['name', 'amount', 'currency'], properties: { name: { description: 'The name of the fare product as displayed to riders.', type: 'string' }, amount: { description: 'The cost of the fare product. May be negative to represent transfer discounts. May be zero to represent a fare product that is free.', type: 'number' }, currency: { description: 'ISO 4217 currency code. The currency of the cost of the fare product.', type: 'string' }, riderCategory: { '$ref': '#/components/schemas/RiderCategory' }, media: { '$ref': '#/components/schemas/FareMedia' } } } as const; export const FareTransferRuleSchema = { type: 'string', enum: ['A_AB', 'A_AB_B', 'AB'] } as const; export const FareTransferSchema = { type: 'object', description: `The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt Terminology: - **Leg**: An itinerary leg as described by the \`Leg\` type of this API description. - **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*. - **Fare Transfer**: A fare transfer groups two or more effective fare legs. - **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer - **B** is any *effective fare leg* following the first *effective fare leg* in this transfer - **AB** are all changes between *effective fare legs* contained in this transfer The fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer: - A_AB means that any product from the first effective fare leg combined with the product attached to the transfer itself (AB) which can be empty (= free). Note that all subsequent effective fare leg products need to be ignored in this case. - A_AB_B mean that a product for each effective fare leg needs to be purchased in a addition to the product attached to the transfer itself (AB) which can be empty (= free) - AB only the transfer product itself has to be purchased. Note that all fare products attached to the contained effective fare legs need to be ignored in this case. An itinerary \`Leg\` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to. `, required: ['effectiveFareLegProducts'], properties: { rule: { '$ref': '#/components/schemas/FareTransferRule' }, transferProducts: { type: 'array', items: { '$ref': '#/components/schemas/FareProduct' } }, effectiveFareLegProducts: { description: `Lists all valid fare products for the effective fare legs. This is an \`array>\` where the inner array lists all possible fare products that would cover this effective fare leg. Each "effective fare leg" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc. You can see the outer array as AND (you need one ticket for each effective fare leg (\`A_AB_B\`), the first effective fare leg (\`A_AB\`) or no fare leg at all but only the transfer product (\`AB\`) and the inner array as OR (you can choose which ticket to buy) `, type: 'array', items: { type: 'array', items: { type: 'array', items: { '$ref': '#/components/schemas/FareProduct' } } } } } } as const; export const ItinerarySchema = { type: 'object', required: ['duration', 'startTime', 'endTime', 'transfers', 'legs'], properties: { duration: { description: 'journey duration in seconds', type: 'integer' }, startTime: { type: 'string', format: 'date-time', description: 'journey departure time' }, endTime: { type: 'string', format: 'date-time', description: 'journey arrival time' }, transfers: { type: 'integer', description: 'The number of transfers this trip has.' }, legs: { description: 'Journey legs', type: 'array', items: { '$ref': '#/components/schemas/Leg' } }, fareTransfers: { description: 'Fare information', type: 'array', items: { '$ref': '#/components/schemas/FareTransfer' } } } } as const; export const TransferSchema = { description: 'transfer from one location to another', type: 'object', required: ['to'], properties: { to: { '$ref': '#/components/schemas/Place' }, default: { type: 'number', description: `optional; missing if the GTFS did not contain a transfer transfer duration in minutes according to GTFS (+heuristics) ` }, foot: { type: 'number', description: `optional; missing if no path was found (timetable / osr) transfer duration in minutes for the foot profile ` }, footRouted: { type: 'number', description: `optional; missing if no path was found with foot routing transfer duration in minutes for the foot profile ` }, wheelchair: { type: 'number', description: `optional; missing if no path was found with the wheelchair profile transfer duration in minutes for the wheelchair profile ` }, wheelchairRouted: { type: 'number', description: `optional; missing if no path was found with the wheelchair profile transfer duration in minutes for the wheelchair profile ` }, wheelchairUsesElevator: { type: 'boolean', description: `optional; missing if no path was found with the wheelchair profile true if the wheelchair path uses an elevator ` }, car: { type: 'number', description: `optional; missing if no path was found with car routing transfer duration in minutes for the car profile ` } } } as const; export const OneToManyParamsSchema = { type: 'object', required: ['one', 'many', 'mode', 'max', 'maxMatchingDistance', 'arriveBy'], properties: { one: { description: 'geo location as latitude;longitude', type: 'string' }, many: { description: `geo locations as latitude;longitude,latitude;longitude,... The number of accepted locations is limited by server config variable \`onetomany_max_many\`. `, type: 'array', items: { type: 'string' }, explode: false }, mode: { description: `routing profile to use (currently supported: \`WALK\`, \`BIKE\`, \`CAR\`) `, '$ref': '#/components/schemas/Mode' }, max: { description: 'maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`.', type: 'number' }, maxMatchingDistance: { description: 'maximum matching distance in meters to match geo coordinates to the street network', type: 'number' }, elevationCosts: { description: `Optional. Default is \`NONE\`. Set an elevation cost profile, to penalize routes with incline. - \`NONE\`: No additional costs for elevations. This is the default behavior - \`LOW\`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - \`HIGH\`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. Elevation cost profiles are currently used by following street modes: - \`BIKE\` `, '$ref': '#/components/schemas/ElevationCosts', default: 'NONE' }, arriveBy: { description: `true = many to one false = one to many `, type: 'boolean' }, withDistance: { description: `If true, the response includes the distance in meters for each path. This requires path reconstruction and may be slower than duration-only queries. `, type: 'boolean', default: false } } } as const; export const OneToManyIntermodalParamsSchema = { type: 'object', required: ['one', 'many'], properties: { one: { description: `\`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id `, type: 'string' }, many: { description: `array of: \`latitude,longitude[,level]\` tuple with - latitude and longitude in degrees - (optional) level: the OSM level (default: 0) OR stop id The number of accepted locations is limited by server config variable \`onetomany_max_many\`. `, type: 'array', items: { type: 'string' }, explode: false }, time: { description: `Optional. Defaults to the current time. Departure time ($arriveBy=false) / arrival date ($arriveBy=true), `, type: 'string', format: 'date-time' }, maxTravelTime: { description: `The maximum travel time in minutes. If not provided, the routing uses the value hardcoded in the server which is usually quite high. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the least transfers) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. `, type: 'integer' }, maxMatchingDistance: { description: 'maximum matching distance in meters to match geo coordinates to the street network', type: 'number', default: 25 }, arriveBy: { description: `Optional. Defaults to false, i.e. one to many search true = many to one false = one to many `, type: 'boolean', default: false }, maxTransfers: { description: `The maximum number of allowed transfers (i.e. interchanges between transit legs, pre- and postTransit do not count as transfers). \`maxTransfers=0\` searches for direct transit connections without any transfers. If you want to search only for non-transit connections (\`FOOT\`, \`CAR\`, etc.), send an empty \`transitModes\` parameter instead. If not provided, the routing uses the server-side default value which is hardcoded and very high to cover all use cases. *Warning*: Use with care. Setting this too low can lead to optimal (e.g. the fastest) journeys not being found. If this value is too low to reach the destination at all, it can lead to slow routing performance. `, type: 'integer' }, minTransferTime: { description: `Optional. Default is 0 minutes. Minimum transfer time for each transfer in minutes. `, type: 'integer', default: 0 }, additionalTransferTime: { description: `Optional. Default is 0 minutes. Additional transfer time reserved for each transfer in minutes. `, type: 'integer', default: 0 }, transferTimeFactor: { description: `Optional. Default is 1.0 Factor to multiply minimum required transfer times with. Values smaller than 1.0 are not supported. `, type: 'number', default: 1 }, useRoutedTransfers: { description: `Optional. Default is \`false\`. Whether to use transfers routed on OpenStreetMap data. `, type: 'boolean', default: false }, pedestrianProfile: { description: `Optional. Default is \`FOOT\`. Accessibility profile to use for pedestrian routing in transfers between transit connections and the first and last mile respectively. `, '$ref': '#/components/schemas/PedestrianProfile', default: 'FOOT' }, pedestrianSpeed: { description: `Optional Average speed for pedestrian routing. `, '$ref': '#/components/schemas/PedestrianSpeed' }, cyclingSpeed: { description: `Optional Average speed for bike routing. `, '$ref': '#/components/schemas/CyclingSpeed' }, elevationCosts: { description: `Optional. Default is \`NONE\`. Set an elevation cost profile, to penalize routes with incline. - \`NONE\`: No additional costs for elevations. This is the default behavior - \`LOW\`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. - \`HIGH\`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. As using an elevation costs profile will increase the travel duration, routing through steep terrain may exceed the maximal allowed duration, causing a location to appear unreachable. Increasing the maximum travel time for these segments may resolve this issue. The profile is used for routing on both the first and last mile. Elevation cost profiles are currently used by following street modes: - \`BIKE\` `, '$ref': '#/components/schemas/ElevationCosts', default: 'NONE' }, transitModes: { description: `Optional. Default is \`TRANSIT\` which allows all transit modes (no restriction). Allowed modes for the transit part. If empty, no transit connections will be computed. For example, this can be used to allow only \`SUBURBAN,SUBWAY,TRAM\`. `, type: 'array', items: { '$ref': '#/components/schemas/Mode' }, default: ['TRANSIT'], explode: false }, preTransitModes: { description: `Optional. Default is \`WALK\`. Does not apply to direct connections (see \`directMode\`). A list of modes that are allowed to be used for the first mile, i.e. from the coordinates to the first transit stop. Example: \`WALK,BIKE_SHARING\`. `, type: 'array', items: { '$ref': '#/components/schemas/Mode' }, default: ['WALK'], explode: false }, postTransitModes: { description: `Optional. Default is \`WALK\`. Does not apply to direct connections (see \`directMode\`). A list of modes that are allowed to be used for the last mile, i.e. from the last transit stop to the target coordinates. Example: \`WALK,BIKE_SHARING\`. `, type: 'array', items: { '$ref': '#/components/schemas/Mode' }, default: ['WALK'], explode: false }, directMode: { description: `Default is \`WALK\` which will compute walking routes as direct connections. Mode used for direction connections from start to destination without using transit. Currently supported non-transit modes: \`WALK\`, \`BIKE\`, \`CAR\` `, '$ref': '#/components/schemas/Mode', default: 'WALK' }, maxPreTransitTime: { description: `Optional. Default is 15min which is \`900\`. Maximum time in seconds for the first street leg. Is limited by server config variable \`street_routing_max_prepost_transit_seconds\`. `, type: 'integer', default: 900, minimum: 0 }, maxPostTransitTime: { description: `Optional. Default is 15min which is \`900\`. Maximum time in seconds for the last street leg. Is limited by server config variable \`street_routing_max_prepost_transit_seconds\`. `, type: 'integer', default: 900, minimum: 0 }, maxDirectTime: { description: `Optional. Default is 30min which is \`1800\`. Maximum time in seconds for direct connections. If a value smaller than either \`maxPreTransitTime\` or \`maxPostTransitTime\` is used, their maximum is set instead. Is limited by server config variable \`street_routing_max_direct_seconds\`. `, type: 'integer', default: 1800, minimum: 0 }, withDistance: { description: `If true, the response includes the distance in meters for each path. This requires path reconstruction and may be slower than duration-only queries. \`withDistance\` is currently limited to street routing. `, type: 'boolean', default: false }, requireBikeTransport: { description: `Optional. Default is \`false\`. If set to \`true\`, all used transit trips are required to allow bike carriage. `, type: 'boolean', default: false }, requireCarTransport: { description: `Optional. Default is \`false\`. If set to \`true\`, all used transit trips are required to allow car carriage. `, type: 'boolean', default: false } } } as const; export const ServerConfigSchema = { Description: 'server configuration', type: 'object', required: ['motisVersion', 'hasElevation', 'hasRoutedTransfers', 'hasStreetRouting', 'maxOneToManySize', 'maxOneToAllTravelTimeLimit', 'maxPrePostTransitTimeLimit', 'maxDirectTimeLimit', 'shapesDebugEnabled'], properties: { motisVersion: { description: 'the version of this MOTIS server', type: 'string' }, hasElevation: { description: 'true if elevation is loaded', type: 'boolean' }, hasRoutedTransfers: { description: 'true if routed transfers available', type: 'boolean' }, hasStreetRouting: { description: 'true if street routing is available', type: 'boolean' }, maxOneToManySize: { description: `limit for the number of \`many\` locations for one-to-many requests `, type: 'number' }, maxOneToAllTravelTimeLimit: { description: 'limit for maxTravelTime API param in minutes', type: 'number' }, maxPrePostTransitTimeLimit: { description: 'limit for maxPrePostTransitTime API param in seconds', type: 'number' }, maxDirectTimeLimit: { description: 'limit for maxDirectTime API param in seconds', type: 'number' }, shapesDebugEnabled: { description: 'true if experimental route shapes debug download API is enabled', type: 'boolean' } } } as const; export const ErrorSchema = { type: 'object', required: ['error'], properties: { error: { type: 'string', description: 'error message' } } } as const; export const RouteSegmentSchema = { description: 'Route segment between two stops to show a route on a map', type: 'object', required: ['from', 'to', 'polyline'], properties: { from: { type: 'integer', description: 'Index into the top-level route stops array' }, to: { type: 'integer', description: 'Index into the top-level route stops array' }, polyline: { type: 'integer', description: 'Index into the top-level route polylines array' } } } as const; export const RoutePolylineSchema = { description: 'Shared polyline used by one or more route segments', type: 'object', required: ['polyline', 'colors', 'routeIndexes'], properties: { polyline: { '$ref': '#/components/schemas/EncodedPolyline' }, colors: { type: 'array', description: 'Unique route colors of routes containing this segment', items: { type: 'string' } }, routeIndexes: { type: 'array', description: 'Indexes into the top-level routes array for routes containing this segment', items: { type: 'integer' } } } } as const; export const RouteColorSchema = { type: 'object', required: ['color', 'textColor'], properties: { color: { type: 'string' }, textColor: { type: 'string' } } } as const; export const RoutePathSourceSchema = { type: 'string', enum: ['NONE', 'TIMETABLE', 'ROUTED'] } as const; export const TransitRouteInfoSchema = { type: 'object', required: ['id', 'shortName', 'longName'], properties: { id: { type: 'string' }, shortName: { type: 'string' }, longName: { type: 'string' }, color: { type: 'string' }, textColor: { type: 'string' } } } as const; export const RouteInfoSchema = { type: 'object', required: ['mode', 'transitRoutes', 'numStops', 'routeIdx', 'pathSource', 'segments'], properties: { mode: { '$ref': '#/components/schemas/Mode', description: 'Transport mode for this route' }, transitRoutes: { type: 'array', items: { '$ref': '#/components/schemas/TransitRouteInfo' } }, numStops: { type: 'integer', description: 'Number of stops along this route' }, routeIdx: { type: 'integer', description: 'Internal route index for debugging purposes' }, pathSource: { '$ref': '#/components/schemas/RoutePathSource' }, segments: { type: 'array', items: { '$ref': '#/components/schemas/RouteSegment' } } } } as const; ================================================ FILE: ui/api/openapi/services.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type Options } from '@hey-api/client-fetch'; import type { PlanData, PlanError, PlanResponse, OneToManyData, OneToManyError, OneToManyResponse, OneToManyPostData, OneToManyPostError, OneToManyPostResponse, OneToManyIntermodalData, OneToManyIntermodalError, OneToManyIntermodalResponse2, OneToManyIntermodalPostData, OneToManyIntermodalPostError, OneToManyIntermodalPostResponse, OneToAllData, OneToAllError, OneToAllResponse, ReverseGeocodeData, ReverseGeocodeError, ReverseGeocodeResponse, GeocodeData, GeocodeError, GeocodeResponse, TripData, TripError, TripResponse, StoptimesData, StoptimesError, StoptimesResponse, TripsData, TripsError, TripsResponse, InitialError, InitialResponse, StopsData, StopsError, StopsResponse, LevelsData, LevelsError, LevelsResponse, RoutesData, RoutesError, RoutesResponse, RouteDetailsData, RouteDetailsError, RouteDetailsResponse, RentalsData, RentalsError, RentalsResponse, TransfersData, TransfersError, TransfersResponse } from './types.gen'; export const client = createClient(createConfig()); /** * Computes optimal connections from one place to another. */ export const plan = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v5/plan' }); }; /** * Street routing from one to many places or many to one. * The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the query. * */ export const oneToMany = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/one-to-many' }); }; /** * Street routing from one to many places or many to one. * The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the request body. * */ export const oneToManyPost = (options: Options) => { return (options?.client ?? client).post({ ...options, url: '/api/v1/one-to-many' }); }; /** * One to many routing * Computes the minimal duration from one place to many or vice versa. * The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the query. * */ export const oneToManyIntermodal = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/experimental/one-to-many-intermodal' }); }; /** * One to many routing * Computes the minimal duration from one place to many or vice versa. * The order in the response array corresponds to the order of coordinates of the \`many\` parameter in the request body. * */ export const oneToManyIntermodalPost = (options: Options) => { return (options?.client ?? client).post({ ...options, url: '/api/experimental/one-to-many-intermodal' }); }; /** * Computes all reachable locations from a given stop within a set duration. * Each result entry will contain the fastest travel duration and the number of connections used. * */ export const oneToAll = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/one-to-all' }); }; /** * Translate coordinates to the closest address(es)/places/stops. */ export const reverseGeocode = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/reverse-geocode' }); }; /** * Autocompletion & geocoding that resolves user input addresses including coordinates */ export const geocode = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/geocode' }); }; /** * Get a trip as itinerary */ export const trip = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v5/trip' }); }; /** * Get the next N departures or arrivals of a stop sorted by time */ export const stoptimes = (options?: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v5/stoptimes' }); }; /** * Given a area frame (box defined by top right and bottom left corner) and a time frame, * it returns all trips and their respective shapes that operate in this area + time frame. * Trips are filtered by zoom level. On low zoom levels, only long distance trains will be shown * while on high zoom levels, also metros, buses and trams will be returned. * */ export const trips = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v5/map/trips' }); }; /** * initial location to view the map at after loading based on where public transport should be visible */ export const initial = (options?: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/map/initial' }); }; /** * Get all stops for a map section */ export const stops = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/map/stops' }); }; /** * Get all available levels for a map section */ export const levels = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/map/levels' }); }; /** * Given an area frame (box defined by the top-right and bottom-left corners), * it returns all routes and their respective shapes that operate within this area. * Routes are filtered by zoom level. On low zoom levels, only long distance trains will be shown * while on high zoom levels, also metros, buses and trams will be returned. * */ export const routes = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/experimental/map/routes' }); }; /** * Returns the full data for a single route, including all stops and * polyline segments. * */ export const routeDetails = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/experimental/map/route-details' }); }; /** * Get a list of rental providers or all rental stations and vehicles for * a map section or provider * * Various options to filter the providers, stations and vehicles are * available. If none of these filters are provided, a list of all * available rental providers is returned without any station, vehicle or * zone data. * * At least one of the following filters must be provided to retrieve * station, vehicle and zone data: * * - A bounding box defined by the `min` and `max` parameters * - A circle defined by the `point` and `radius` parameters * - A list of provider groups via the `providerGroups` parameter * - A list of providers via the `providers` parameter * * Only data that matches all the provided filters is returned. * * Provide the `withProviders=false` parameter to retrieve only provider * groups if detailed feed information is not required. * */ export const rentals = (options?: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/v1/rentals' }); }; /** * Prints all transfers of a timetable location (track, bus stop, etc.) */ export const transfers = (options: Options) => { return (options?.client ?? client).get({ ...options, url: '/api/debug/transfers' }); }; ================================================ FILE: ui/api/openapi/types.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts /** * Cause of this alert. */ export type AlertCause = 'UNKNOWN_CAUSE' | 'OTHER_CAUSE' | 'TECHNICAL_PROBLEM' | 'STRIKE' | 'DEMONSTRATION' | 'ACCIDENT' | 'HOLIDAY' | 'WEATHER' | 'MAINTENANCE' | 'CONSTRUCTION' | 'POLICE_ACTIVITY' | 'MEDICAL_EMERGENCY'; /** * The effect of this problem on the affected entity. */ export type AlertEffect = 'NO_SERVICE' | 'REDUCED_SERVICE' | 'SIGNIFICANT_DELAYS' | 'DETOUR' | 'ADDITIONAL_SERVICE' | 'MODIFIED_SERVICE' | 'OTHER_EFFECT' | 'UNKNOWN_EFFECT' | 'STOP_MOVED' | 'NO_EFFECT' | 'ACCESSIBILITY_ISSUE'; /** * The severity of the alert. */ export type AlertSeverityLevel = 'UNKNOWN_SEVERITY' | 'INFO' | 'WARNING' | 'SEVERE'; /** * A time interval. * The interval is considered active at time t if t is greater than or equal to the start time and less than the end time. * */ export type TimeRange = { /** * If missing, the interval starts at minus infinity. * If a TimeRange is provided, either start or end must be provided - both fields cannot be empty. * */ start: string; /** * If missing, the interval ends at plus infinity. * If a TimeRange is provided, either start or end must be provided - both fields cannot be empty. * */ end: string; }; /** * An alert, indicating some sort of incident in the public transit network. */ export type Alert = { /** * Attribute or notice code (e.g. for HRDF or NeTEx) */ code?: string; /** * Time when the alert should be shown to the user. * If missing, the alert will be shown as long as it appears in the feed. * If multiple ranges are given, the alert will be shown during all of them. * */ communicationPeriod?: Array; /** * Time when the services are affected by the disruption mentioned in the alert. */ impactPeriod?: Array; cause?: AlertCause; /** * Description of the cause of the alert that allows for agency-specific language; * more specific than the Cause. * */ causeDetail?: string; effect?: AlertEffect; /** * Description of the effect of the alert that allows for agency-specific language; * more specific than the Effect. * */ effectDetail?: string; /** * The URL which provides additional information about the alert. */ url?: string; /** * Header for the alert. This plain-text string will be highlighted, for example in boldface. * */ headerText: string; /** * Description for the alert. * This plain-text string will be formatted as the body of the alert (or shown on an explicit "expand" request by the user). * The information in the description should add to the information of the header. * */ descriptionText: string; /** * Text containing the alert's header to be used for text-to-speech implementations. * This field is the text-to-speech version of header_text. * It should contain the same information as headerText but formatted such that it can read as text-to-speech * (for example, abbreviations removed, numbers spelled out, etc.) * */ ttsHeaderText?: string; /** * Text containing a description for the alert to be used for text-to-speech implementations. * This field is the text-to-speech version of description_text. * It should contain the same information as description_text but formatted such that it can be read as text-to-speech * (for example, abbreviations removed, numbers spelled out, etc.) * */ ttsDescriptionText?: string; /** * Severity of the alert. */ severityLevel?: AlertSeverityLevel; /** * String containing an URL linking to an image. */ imageUrl?: string; /** * IANA media type as to specify the type of image to be displayed. The type must start with "image/" * */ imageMediaType?: string; /** * Text describing the appearance of the linked image in the image field * (e.g., in case the image can't be displayed or the user can't see the image for accessibility reasons). * See the HTML spec for alt image text. * */ imageAlternativeText?: string; }; /** * Object containing duration if a path was found or none if no path was found */ export type Duration = { /** * duration in seconds if a path was found, otherwise missing */ duration?: number; /** * distance in meters if a path was found and distance computation was requested, otherwise missing */ distance?: number; }; /** * Object containing a single element of a ParetoSet */ export type ParetoSetEntry = { /** * duration in seconds for the the best solution using `transfer` transfers * * Notice that the resolution is currently in minutes, because of implementation details * */ duration: number; /** * The minimal number of transfers required to arrive within `duration` seconds * * transfers=0: Direct transit connecion without any transfers * transfers=1: Transit connection with 1 transfer * */ transfers: number; }; /** * Pareto set of optimal transit solutions */ export type ParetoSet = Array; /** * Object containing the optimal street and transit durations for One-to-Many routing */ export type OneToManyIntermodalResponse = { /** * Fastest durations for street routing * The order of the items corresponds to the order of the `many` locations * If no street routed connection is found, the corresponding `Duration` will be empty * */ street_durations?: Array; /** * Pareto optimal solutions * The order of the items corresponds to the order of the `many` locations * If no connection using transits is found, the corresponding `ParetoSet` will be empty * */ transit_durations?: Array; }; /** * Administrative area */ export type Area = { /** * Name of the area */ name: string; /** * [OpenStreetMap `admin_level`](https://wiki.openstreetmap.org/wiki/Key:admin_level) * of the area * */ adminLevel: number; /** * Whether this area was matched by the input text */ matched: boolean; /** * Set for the first area after the `default` area that distinguishes areas * if the match is ambiguous regarding (`default` area + place name / street [+ house number]). * */ unique?: boolean; /** * Whether this area should be displayed as default area (area with admin level closest 7) */ default?: boolean; }; /** * Matched token range (from index, length) */ export type Token = [ number, number ]; /** * location type */ export type LocationType = 'ADDRESS' | 'PLACE' | 'STOP'; /** * # Street modes * * - `WALK` * - `BIKE` * - `RENTAL` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs. * - `CAR` * - `CAR_PARKING` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs. * - `CAR_DROPOFF` Experimental. Expect unannounced breaking changes (without version bumps) for all perameters and returned structs. * - `ODM` on-demand taxis from the Prima+ÖV Project * - `RIDE_SHARING` ride sharing from the Prima+ÖV Project * - `FLEX` flexible transports * * # Transit modes * * - `TRANSIT`: translates to `TRAM,FERRY,AIRPLANE,BUS,COACH,RAIL,ODM,FUNICULAR,AERIAL_LIFT,OTHER` * - `TRAM`: trams * - `SUBWAY`: subway trains (Paris Metro, London Underground, but also NYC Subway, Hamburger Hochbahn, and other non-underground services) * - `FERRY`: ferries * - `AIRPLANE`: airline flights * - `BUS`: short distance buses (does not include `COACH`) * - `COACH`: long distance buses (does not include `BUS`) * - `RAIL`: translates to `HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN,SUBWAY` * - `HIGHSPEED_RAIL`: long distance high speed trains (e.g. TGV) * - `LONG_DISTANCE`: long distance inter city trains * - `NIGHT_RAIL`: long distance night trains * - `REGIONAL_FAST_RAIL`: deprecated, `REGIONAL_RAIL` will be used * - `REGIONAL_RAIL`: regional train * - `SUBURBAN`: suburban trains (e.g. S-Bahn, RER, Elizabeth Line, ...) * - `ODM`: demand responsive transport * - `FUNICULAR`: Funicular. Any rail system designed for steep inclines. * - `AERIAL_LIFT`: Aerial lift, suspended cable car (e.g., gondola lift, aerial tramway). Cable transport where cabins, cars, gondolas or open chairs are suspended by means of one or more cables. * - `AREAL_LIFT`: deprecated * - `METRO`: deprecated * - `CABLE_CAR`: deprecated * */ export type Mode = 'WALK' | 'BIKE' | 'RENTAL' | 'CAR' | 'CAR_PARKING' | 'CAR_DROPOFF' | 'ODM' | 'RIDE_SHARING' | 'FLEX' | 'DEBUG_BUS_ROUTE' | 'DEBUG_RAILWAY_ROUTE' | 'DEBUG_FERRY_ROUTE' | 'TRANSIT' | 'TRAM' | 'SUBWAY' | 'FERRY' | 'AIRPLANE' | 'BUS' | 'COACH' | 'RAIL' | 'HIGHSPEED_RAIL' | 'LONG_DISTANCE' | 'NIGHT_RAIL' | 'REGIONAL_FAST_RAIL' | 'REGIONAL_RAIL' | 'SUBURBAN' | 'FUNICULAR' | 'AERIAL_LIFT' | 'OTHER' | 'AREAL_LIFT' | 'METRO' | 'CABLE_CAR'; /** * GeoCoding match */ export type Match = { type: LocationType; /** * Experimental. Type categories might be adjusted. * * For OSM stop locations: the amenity type based on * https://wiki.openstreetmap.org/wiki/OpenStreetMap_Carto/Symbols * */ category?: string; /** * list of non-overlapping tokens that were matched */ tokens: Array; /** * name of the location (transit stop / PoI / address) */ name: string; /** * unique ID of the location */ id: string; /** * latitude */ lat: number; /** * longitude */ lon: number; /** * level according to OpenStreetMap * (at the moment only for public transport) * */ level?: number; /** * street name */ street?: string; /** * house number */ houseNumber?: string; /** * ISO3166-1 country code from OpenStreetMap */ country?: string; /** * zip code */ zip?: string; /** * timezone name (e.g. "Europe/Berlin") */ tz?: string; /** * list of areas */ areas: Array; /** * score according to the internal scoring system (the scoring algorithm might change in the future) */ score: number; /** * available transport modes for stops */ modes?: Array; /** * importance of a stop, normalized from [0, 1] */ importance?: number; }; /** * Different elevation cost profiles for street routing. * Using a elevation cost profile will prefer routes with a smaller incline and smaller difference in elevation, even if the routed way is longer. * * - `NONE`: Ignore elevation data for routing. This is the default behavior * - `LOW`: Add a low penalty for inclines. This will favor longer paths, if the elevation increase and incline are smaller. * - `HIGH`: Add a high penalty for inclines. This will favor even longer paths, if the elevation increase and incline are smaller. * */ export type ElevationCosts = 'NONE' | 'LOW' | 'HIGH'; /** * Different accessibility profiles for pedestrians. */ export type PedestrianProfile = 'FOOT' | 'WHEELCHAIR'; /** * Average speed for pedestrian routing in meters per second */ export type PedestrianSpeed = number; /** * Average speed for bike routing in meters per second */ export type CyclingSpeed = number; /** * - `NORMAL` - latitude / longitude coordinate or address * - `BIKESHARE` - bike sharing station * - `TRANSIT` - transit stop * */ export type VertexType = 'NORMAL' | 'BIKESHARE' | 'TRANSIT'; /** * - `NORMAL` - entry/exit is possible normally * - `NOT_ALLOWED` - entry/exit is not allowed * */ export type PickupDropoffType = 'NORMAL' | 'NOT_ALLOWED'; export type Place = { /** * name of the transit stop / PoI / address */ name: string; /** * The ID of the stop. This is often something that users don't care about. */ stopId?: string; /** * If it's not a root stop, this field contains the `stopId` of the parent stop. */ parentId?: string; /** * The importance of the stop between 0-1. */ importance?: number; /** * latitude */ lat: number; /** * longitude */ lon: number; /** * level according to OpenStreetMap */ level: number; /** * timezone name (e.g. "Europe/Berlin") */ tz?: string; /** * arrival time */ arrival?: string; /** * departure time */ departure?: string; /** * scheduled arrival time */ scheduledArrival?: string; /** * scheduled departure time */ scheduledDeparture?: string; /** * scheduled track from the static schedule timetable dataset */ scheduledTrack?: string; /** * The current track/platform information, updated with real-time updates if available. * Can be missing if neither real-time updates nor the schedule timetable contains track information. * */ track?: string; /** * description of the location that provides more detailed information */ description?: string; vertexType?: VertexType; /** * Type of pickup. It could be disallowed due to schedule, skipped stops or cancellations. */ pickupType?: PickupDropoffType; /** * Type of dropoff. It could be disallowed due to schedule, skipped stops or cancellations. */ dropoffType?: PickupDropoffType; /** * Whether this stop is cancelled due to the realtime situation. */ cancelled?: boolean; /** * Alerts for this stop. */ alerts?: Array; /** * for `FLEX` transports, the flex location area or location group name */ flex?: string; /** * for `FLEX` transports, the flex location area ID or location group ID */ flexId?: string; /** * Time that on-demand service becomes available */ flexStartPickupDropOffWindow?: string; /** * Time that on-demand service ends */ flexEndPickupDropOffWindow?: string; /** * available transport modes for stops */ modes?: Array; }; /** * Place reachable by One-to-All */ export type ReachablePlace = { /** * Place reached by One-to-All */ place?: Place; /** * Total travel duration */ duration?: number; /** * k is the smallest number, for which a journey with the shortest duration and at most k-1 transfers exist. * You can think of k as the number of connections used. * * In more detail: * * k=0: No connection, e.g. for the one location * k=1: Direct connection * k=2: Connection with 1 transfer * */ k?: number; }; /** * Object containing all reachable places by One-to-All search */ export type Reachable = { /** * One location used in One-to-All search */ one?: Place; /** * List of locations reachable by One-to-All */ all?: Array; }; /** * departure or arrival event at a stop */ export type StopTime = { /** * information about the stop place and time */ place: Place; /** * Transport mode for this leg */ mode: Mode; /** * Whether there is real-time data about this leg */ realTime: boolean; /** * The headsign of the bus or train being used. * For non-transit legs, null * */ headsign: string; /** * first stop of this trip */ tripFrom: Place; /** * final stop of this trip */ tripTo: Place; agencyId: string; agencyName: string; agencyUrl: string; routeId: string; routeUrl?: string; directionId: string; routeColor?: string; routeTextColor?: string; tripId: string; routeType?: number; routeShortName: string; routeLongName: string; tripShortName: string; displayName: string; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Stops on the trips before this stop. Returned only if `fetchStop` and `arriveBy` are `true`. * */ previousStops?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Stops on the trips after this stop. Returned only if `fetchStop` is `true` and `arriveBy` is `false`. * */ nextStops?: Array; /** * Type of pickup (for departures) or dropoff (for arrivals), may be disallowed either due to schedule, skipped stops or cancellations */ pickupDropoffType: PickupDropoffType; /** * Whether the departure/arrival is cancelled due to the realtime situation (either because the stop is skipped or because the entire trip is cancelled). */ cancelled: boolean; /** * Whether the entire trip is cancelled due to the realtime situation. */ tripCancelled: boolean; /** * Filename and line number where this trip is from */ source: string; }; /** * trip id and name */ export type TripInfo = { /** * trip ID (dataset trip id prefixed with the dataset tag) */ tripId: string; /** * trip display name (api version < 4) */ routeShortName?: string; /** * trip display name (api version >= 4) */ displayName?: string; }; /** * trip segment between two stops to show a trip on a map */ export type TripSegment = { trips: Array; routeColor?: string; /** * Transport mode for this leg */ mode: Mode; /** * distance in meters */ distance: number; from: Place; to: Place; /** * departure time */ departure: string; /** * arrival time */ arrival: string; /** * scheduled departure time */ scheduledDeparture: string; /** * scheduled arrival time */ scheduledArrival: string; /** * Whether there is real-time data about this leg */ realTime: boolean; /** * Google polyline encoded coordinate sequence (with precision 5) where the trip travels on this segment. */ polyline: string; }; export type Direction = 'DEPART' | 'HARD_LEFT' | 'LEFT' | 'SLIGHTLY_LEFT' | 'CONTINUE' | 'SLIGHTLY_RIGHT' | 'RIGHT' | 'HARD_RIGHT' | 'CIRCLE_CLOCKWISE' | 'CIRCLE_COUNTERCLOCKWISE' | 'STAIRS' | 'ELEVATOR' | 'UTURN_LEFT' | 'UTURN_RIGHT'; export type EncodedPolyline = { /** * The encoded points of the polyline using the Google polyline encoding. */ points: string; /** * The precision of the returned polyline (7 for /v1, 6 for /v2) * Be aware that with precision 7, coordinates with |longitude| > 107.37 are undefined/will overflow. * */ precision: number; /** * The number of points in the string */ length: number; }; export type StepInstruction = { relativeDirection: Direction; /** * The distance in meters that this step takes. */ distance: number; /** * level where this segment starts, based on OpenStreetMap data */ fromLevel: number; /** * level where this segment starts, based on OpenStreetMap data */ toLevel: number; /** * OpenStreetMap way index */ osmWay?: number; polyline: EncodedPolyline; /** * The name of the street. */ streetName: string; /** * Not implemented! * When exiting a highway or traffic circle, the exit name/number. * */ exit: string; /** * Not implemented! * Indicates whether or not a street changes direction at an intersection. * */ stayOn: boolean; /** * Not implemented! * This step is on an open area, such as a plaza or train platform, * and thus the directions should say something like "cross" * */ area: boolean; /** * Indicates that a fee must be paid by general traffic to use a road, road bridge or road tunnel. */ toll?: boolean; /** * Experimental. Indicates whether access to this part of the route is restricted. * See: https://wiki.openstreetmap.org/wiki/Conditional_restrictions * */ accessRestriction?: string; /** * incline in meters across this path segment */ elevationUp?: number; /** * decline in meters across this path segment */ elevationDown?: number; }; export type RentalFormFactor = 'BICYCLE' | 'CARGO_BICYCLE' | 'CAR' | 'MOPED' | 'SCOOTER_STANDING' | 'SCOOTER_SEATED' | 'OTHER'; export type RentalPropulsionType = 'HUMAN' | 'ELECTRIC_ASSIST' | 'ELECTRIC' | 'COMBUSTION' | 'COMBUSTION_DIESEL' | 'HYBRID' | 'PLUG_IN_HYBRID' | 'HYDROGEN_FUEL_CELL'; export type RentalReturnConstraint = 'NONE' | 'ANY_STATION' | 'ROUNDTRIP_STATION'; /** * Vehicle rental */ export type Rental = { /** * Rental provider ID */ providerId: string; /** * Rental provider group ID */ providerGroupId: string; /** * Vehicle share system ID */ systemId: string; /** * Vehicle share system name */ systemName?: string; /** * URL of the vehicle share system */ url?: string; /** * Color associated with this provider, in hexadecimal RGB format * (e.g. "#FF0000" for red). Can be empty. * */ color?: string; /** * Name of the station */ stationName?: string; /** * Name of the station where the vehicle is picked up (empty for free floating vehicles) */ fromStationName?: string; /** * Name of the station where the vehicle is returned (empty for free floating vehicles) */ toStationName?: string; /** * Rental URI for Android (deep link to the specific station or vehicle) */ rentalUriAndroid?: string; /** * Rental URI for iOS (deep link to the specific station or vehicle) */ rentalUriIOS?: string; /** * Rental URI for web (deep link to the specific station or vehicle) */ rentalUriWeb?: string; formFactor?: RentalFormFactor; propulsionType?: RentalPropulsionType; returnConstraint?: RentalReturnConstraint; }; /** * A multi-polygon contains a number of polygons, each containing a number * of rings, which are encoded as polylines (with precision 6). * * For each polygon, the first ring is the outer ring, all subsequent rings * are inner rings (holes). * */ export type MultiPolygon = Array>; export type RentalZoneRestrictions = { /** * List of vehicle types (as indices into the provider's vehicle types * array) to which these restrictions apply. * If empty, the restrictions apply to all vehicle types of the provider. * */ vehicleTypeIdxs: Array<(number)>; /** * whether the ride is allowed to start in this zone */ rideStartAllowed: boolean; /** * whether the ride is allowed to end in this zone */ rideEndAllowed: boolean; /** * whether the ride is allowed to pass through this zone */ rideThroughAllowed: boolean; /** * whether vehicles can only be parked at stations in this zone */ stationParking?: boolean; }; export type RentalVehicleType = { /** * Unique identifier of the vehicle type (unique within the provider) */ id: string; /** * Public name of the vehicle type (can be empty) */ name?: string; formFactor: RentalFormFactor; propulsionType: RentalPropulsionType; returnConstraint: RentalReturnConstraint; /** * Whether the return constraint was guessed instead of being specified by the rental provider */ returnConstraintGuessed: boolean; }; export type RentalProvider = { /** * Unique identifier of the rental provider */ id: string; /** * Name of the provider to be displayed to customers */ name: string; /** * Id of the rental provider group this provider belongs to */ groupId: string; /** * Name of the system operator */ operator?: string; /** * URL of the vehicle share system */ url?: string; /** * URL where a customer can purchase a membership */ purchaseUrl?: string; /** * Color associated with this provider, in hexadecimal RGB format * (e.g. "#FF0000" for red). Can be empty. * */ color?: string; /** * Bounding box of the area covered by this rental provider, * [west, south, east, north] as [lon, lat, lon, lat] * */ bbox: [ number, number, number, number ]; vehicleTypes: Array; /** * List of form factors offered by this provider */ formFactors: Array; defaultRestrictions: RentalZoneRestrictions; globalGeofencingRules: Array; }; export type RentalProviderGroup = { /** * Unique identifier of the rental provider group */ id: string; /** * Name of the provider group to be displayed to customers */ name: string; /** * Color associated with this provider group, in hexadecimal RGB format * (e.g. "#FF0000" for red). Can be empty. * */ color?: string; /** * List of rental provider IDs that belong to this group */ providers: Array<(string)>; /** * List of form factors offered by this provider group */ formFactors: Array; }; export type RentalStation = { /** * Unique identifier of the rental station */ id: string; /** * Unique identifier of the rental provider */ providerId: string; /** * Unique identifier of the rental provider group */ providerGroupId: string; /** * Public name of the station */ name: string; /** * latitude */ lat: number; /** * longitude */ lon: number; /** * Address where the station is located */ address?: string; /** * Cross street or landmark where the station is located */ crossStreet?: string; /** * Rental URI for Android (deep link to the specific station) */ rentalUriAndroid?: string; /** * Rental URI for iOS (deep link to the specific station) */ rentalUriIOS?: string; /** * Rental URI for web (deep link to the specific station) */ rentalUriWeb?: string; /** * true if vehicles can be rented from this station, false if it is temporarily out of service */ isRenting: boolean; /** * true if vehicles can be returned to this station, false if it is temporarily out of service */ isReturning: boolean; /** * Number of vehicles available for rental at this station */ numVehiclesAvailable: number; /** * List of form factors available for rental and/or return at this station */ formFactors: Array; /** * List of vehicle types currently available at this station (vehicle type ID -> count) */ vehicleTypesAvailable: { [key: string]: (number); }; /** * List of vehicle docks currently available at this station (vehicle type ID -> count) */ vehicleDocksAvailable: { [key: string]: (number); }; stationArea?: MultiPolygon; /** * Bounding box of the area covered by this station, * [west, south, east, north] as [lon, lat, lon, lat] * */ bbox: [ number, number, number, number ]; }; export type RentalVehicle = { /** * Unique identifier of the rental vehicle */ id: string; /** * Unique identifier of the rental provider */ providerId: string; /** * Unique identifier of the rental provider group */ providerGroupId: string; /** * Vehicle type ID (unique within the provider) */ typeId: string; /** * latitude */ lat: number; /** * longitude */ lon: number; formFactor: RentalFormFactor; propulsionType: RentalPropulsionType; returnConstraint: RentalReturnConstraint; /** * Station ID if the vehicle is currently at a station */ stationId?: string; /** * Station ID where the vehicle must be returned, if applicable */ homeStationId?: string; /** * true if the vehicle is currently reserved by a customer, false otherwise */ isReserved: boolean; /** * true if the vehicle is out of service, false otherwise */ isDisabled: boolean; /** * Rental URI for Android (deep link to the specific vehicle) */ rentalUriAndroid?: string; /** * Rental URI for iOS (deep link to the specific vehicle) */ rentalUriIOS?: string; /** * Rental URI for web (deep link to the specific vehicle) */ rentalUriWeb?: string; }; export type RentalZone = { /** * Unique identifier of the rental provider */ providerId: string; /** * Unique identifier of the rental provider group */ providerGroupId: string; /** * Public name of the geofencing zone */ name?: string; /** * Zone precedence / z-index (higher number = higher precedence) */ z: number; /** * Bounding box of the area covered by this zone, * [west, south, east, north] as [lon, lat, lon, lat] * */ bbox: [ number, number, number, number ]; area: MultiPolygon; rules: Array; }; /** * not available for GTFS datasets by default * For NeTEx it contains information about the vehicle category, e.g. IC/InterCity * */ export type Category = { id: string; name: string; shortName: string; }; export type Leg = { /** * Transport mode for this leg */ mode: Mode; from: Place; to: Place; /** * Leg duration in seconds * * If leg is footpath: * The footpath duration is derived from the default footpath * duration using the query parameters `transferTimeFactor` and * `additionalTransferTime` as follows: * `leg.duration = defaultDuration * transferTimeFactor + additionalTransferTime.` * In case the defaultDuration is needed, it can be calculated by * `defaultDuration = (leg.duration - additionalTransferTime) / transferTimeFactor`. * Note that the default values are `transferTimeFactor = 1` and * `additionalTransferTime = 0` in case they are not explicitly * provided in the query. * */ duration: number; /** * leg departure time */ startTime: string; /** * leg arrival time */ endTime: string; /** * scheduled leg departure time */ scheduledStartTime: string; /** * scheduled leg arrival time */ scheduledEndTime: string; /** * Whether there is real-time data about this leg */ realTime: boolean; /** * Whether this leg was originally scheduled to run or is an additional service. * Scheduled times will equal realtime times in this case. * */ scheduled: boolean; /** * For non-transit legs the distance traveled while traversing this leg in meters. */ distance?: number; /** * For transit legs, if the rider should stay on the vehicle as it changes route names. */ interlineWithPreviousLeg?: boolean; /** * For transit legs, the headsign of the bus or train being used. * For non-transit legs, null * */ headsign?: string; /** * first stop of this trip */ tripFrom?: Place; /** * final stop of this trip (can differ from headsign) */ tripTo?: Place; category?: Category; routeId?: string; routeUrl?: string; directionId?: string; routeColor?: string; routeTextColor?: string; routeType?: number; agencyName?: string; agencyUrl?: string; agencyId?: string; tripId?: string; routeShortName?: string; routeLongName?: string; tripShortName?: string; displayName?: string; /** * Whether this trip is cancelled */ cancelled?: boolean; /** * Filename and line number where this trip is from */ source?: string; /** * For transit legs, intermediate stops between the Place where the leg originates * and the Place where the leg ends. For non-transit legs, null. * */ intermediateStops?: Array; /** * Encoded geometry of the leg. * If detailed leg output is disabled, this is returned as an empty * polyline. * */ legGeometry: EncodedPolyline; /** * A series of turn by turn instructions * used for walking, biking and driving. * This field is omitted if the request disables detailed leg output. * */ steps?: Array; rental?: Rental; /** * Index into `Itinerary.fareTransfers` array * to identify which fare transfer this leg belongs to * */ fareTransferIndex?: number; /** * Index into the `Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts` array * to identify which effective fare leg this itinerary leg belongs to * */ effectiveFareLegIndex?: number; /** * Alerts for this stop. */ alerts?: Array; /** * If set, this attribute indicates that this trip has been expanded * beyond the feed end date (enabled by config flag `timetable.dataset.extend_calendar`) * by looping active weekdays, e.g. from calendar.txt in GTFS. * */ loopedCalendarSince?: string; /** * Whether bikes can be carried on this leg. * */ bikesAllowed?: boolean; }; export type RiderCategory = { /** * Rider category name as displayed to the rider. */ riderCategoryName: string; /** * Specifies if this category should be considered the default (i.e. the main category displayed to riders). */ isDefaultFareCategory: boolean; /** * URL to a web page providing detailed information about the rider category and/or its eligibility criteria. */ eligibilityUrl?: string; }; /** * - `NONE`: No fare media involved (e.g., cash payment) * - `PAPER_TICKET`: Physical paper ticket * - `TRANSIT_CARD`: Physical transit card with stored value * - `CONTACTLESS_EMV`: cEMV (contactless payment) * - `MOBILE_APP`: Mobile app with virtual transit cards/passes * */ export type FareMediaType = 'NONE' | 'PAPER_TICKET' | 'TRANSIT_CARD' | 'CONTACTLESS_EMV' | 'MOBILE_APP'; export type FareMedia = { /** * Name of the fare media. Required for transit cards and mobile apps. */ fareMediaName?: string; /** * The type of fare media. */ fareMediaType: FareMediaType; }; export type FareProduct = { /** * The name of the fare product as displayed to riders. */ name: string; /** * The cost of the fare product. May be negative to represent transfer discounts. May be zero to represent a fare product that is free. */ amount: number; /** * ISO 4217 currency code. The currency of the cost of the fare product. */ currency: string; riderCategory?: RiderCategory; media?: FareMedia; }; export type FareTransferRule = 'A_AB' | 'A_AB_B' | 'AB'; /** * The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt * * Terminology: * - **Leg**: An itinerary leg as described by the `Leg` type of this API description. * - **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*. * - **Fare Transfer**: A fare transfer groups two or more effective fare legs. * - **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer * - **B** is any *effective fare leg* following the first *effective fare leg* in this transfer * - **AB** are all changes between *effective fare legs* contained in this transfer * * The fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer: * - A_AB means that any product from the first effective fare leg combined with the product attached to the transfer itself (AB) which can be empty (= free). Note that all subsequent effective fare leg products need to be ignored in this case. * - A_AB_B mean that a product for each effective fare leg needs to be purchased in a addition to the product attached to the transfer itself (AB) which can be empty (= free) * - AB only the transfer product itself has to be purchased. Note that all fare products attached to the contained effective fare legs need to be ignored in this case. * * An itinerary `Leg` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to. * */ export type FareTransfer = { rule?: FareTransferRule; transferProducts?: Array; /** * Lists all valid fare products for the effective fare legs. * This is an `array>` where the inner array * lists all possible fare products that would cover this effective fare leg. * Each "effective fare leg" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc. * You can see the outer array as AND (you need one ticket for each effective fare leg (`A_AB_B`), the first effective fare leg (`A_AB`) or no fare leg at all but only the transfer product (`AB`) * and the inner array as OR (you can choose which ticket to buy) * */ effectiveFareLegProducts: Array>>; }; export type Itinerary = { /** * journey duration in seconds */ duration: number; /** * journey departure time */ startTime: string; /** * journey arrival time */ endTime: string; /** * The number of transfers this trip has. */ transfers: number; /** * Journey legs */ legs: Array; /** * Fare information */ fareTransfers?: Array; }; /** * transfer from one location to another */ export type Transfer = { to: Place; /** * optional; missing if the GTFS did not contain a transfer * transfer duration in minutes according to GTFS (+heuristics) * */ default?: number; /** * optional; missing if no path was found (timetable / osr) * transfer duration in minutes for the foot profile * */ foot?: number; /** * optional; missing if no path was found with foot routing * transfer duration in minutes for the foot profile * */ footRouted?: number; /** * optional; missing if no path was found with the wheelchair profile * transfer duration in minutes for the wheelchair profile * */ wheelchair?: number; /** * optional; missing if no path was found with the wheelchair profile * transfer duration in minutes for the wheelchair profile * */ wheelchairRouted?: number; /** * optional; missing if no path was found with the wheelchair profile * true if the wheelchair path uses an elevator * */ wheelchairUsesElevator?: boolean; /** * optional; missing if no path was found with car routing * transfer duration in minutes for the car profile * */ car?: number; }; export type OneToManyParams = { /** * geo location as latitude;longitude */ one: string; /** * geo locations as latitude;longitude,latitude;longitude,... * * The number of accepted locations is limited by server config variable `onetomany_max_many`. * */ many: Array<(string)>; /** * routing profile to use (currently supported: \`WALK\`, \`BIKE\`, \`CAR\`) * */ mode: Mode; /** * maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`. */ max: number; /** * maximum matching distance in meters to match geo coordinates to the street network */ maxMatchingDistance: number; /** * Optional. Default is `NONE`. * * Set an elevation cost profile, to penalize routes with incline. * - `NONE`: No additional costs for elevations. This is the default behavior * - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. * - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. * * As using an elevation costs profile will increase the travel duration, * routing through steep terrain may exceed the maximal allowed duration, * causing a location to appear unreachable. * Increasing the maximum travel time for these segments may resolve this issue. * * Elevation cost profiles are currently used by following street modes: * - `BIKE` * */ elevationCosts?: ElevationCosts; /** * true = many to one * false = one to many * */ arriveBy: boolean; /** * If true, the response includes the distance in meters * for each path. This requires path reconstruction and * may be slower than duration-only queries. * */ withDistance?: boolean; }; export type OneToManyIntermodalParams = { /** * \`latitude,longitude[,level]\` tuple with * - latitude and longitude in degrees * - (optional) level: the OSM level (default: 0) * * OR * * stop id * */ one: string; /** * array of: * * \`latitude,longitude[,level]\` tuple with * - latitude and longitude in degrees * - (optional) level: the OSM level (default: 0) * * OR * * stop id * * The number of accepted locations is limited by server config variable `onetomany_max_many`. * */ many: Array<(string)>; /** * Optional. Defaults to the current time. * * Departure time ($arriveBy=false) / arrival date ($arriveBy=true), * */ time?: string; /** * The maximum travel time in minutes. * If not provided, the routing uses the value * hardcoded in the server which is usually quite high. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the least transfers) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * */ maxTravelTime?: number; /** * maximum matching distance in meters to match geo coordinates to the street network */ maxMatchingDistance?: number; /** * Optional. Defaults to false, i.e. one to many search * * true = many to one * false = one to many * */ arriveBy?: boolean; /** * The maximum number of allowed transfers (i.e. interchanges between transit legs, * pre- and postTransit do not count as transfers). * `maxTransfers=0` searches for direct transit connections without any transfers. * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), * send an empty `transitModes` parameter instead. * * If not provided, the routing uses the server-side default value * which is hardcoded and very high to cover all use cases. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the fastest) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * */ maxTransfers?: number; /** * Optional. Default is 0 minutes. * * Minimum transfer time for each transfer in minutes. * */ minTransferTime?: number; /** * Optional. Default is 0 minutes. * * Additional transfer time reserved for each transfer in minutes. * */ additionalTransferTime?: number; /** * Optional. Default is 1.0 * * Factor to multiply minimum required transfer times with. * Values smaller than 1.0 are not supported. * */ transferTimeFactor?: number; /** * Optional. Default is `false`. * * Whether to use transfers routed on OpenStreetMap data. * */ useRoutedTransfers?: boolean; /** * Optional. Default is `FOOT`. * * Accessibility profile to use for pedestrian routing in transfers * between transit connections and the first and last mile respectively. * */ pedestrianProfile?: PedestrianProfile; /** * Optional * * Average speed for pedestrian routing. * */ pedestrianSpeed?: PedestrianSpeed; /** * Optional * * Average speed for bike routing. * */ cyclingSpeed?: CyclingSpeed; /** * Optional. Default is `NONE`. * * Set an elevation cost profile, to penalize routes with incline. * - `NONE`: No additional costs for elevations. This is the default behavior * - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. * - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. * * As using an elevation costs profile will increase the travel duration, * routing through steep terrain may exceed the maximal allowed duration, * causing a location to appear unreachable. * Increasing the maximum travel time for these segments may resolve this issue. * * The profile is used for routing on both the first and last mile. * * Elevation cost profiles are currently used by following street modes: * - `BIKE` * */ elevationCosts?: ElevationCosts; /** * Optional. Default is `TRANSIT` which allows all transit modes (no restriction). * Allowed modes for the transit part. If empty, no transit connections will be computed. * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. * */ transitModes?: Array; /** * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). * * A list of modes that are allowed to be used for the first mile, i.e. from the coordinates to the first transit stop. Example: `WALK,BIKE_SHARING`. * */ preTransitModes?: Array; /** * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). * * A list of modes that are allowed to be used for the last mile, i.e. from the last transit stop to the target coordinates. Example: `WALK,BIKE_SHARING`. * */ postTransitModes?: Array; /** * Default is `WALK` which will compute walking routes as direct connections. * * Mode used for direction connections from start to destination without using transit. * * Currently supported non-transit modes: \`WALK\`, \`BIKE\`, \`CAR\` * */ directMode?: Mode; /** * Optional. Default is 15min which is `900`. * Maximum time in seconds for the first street leg. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPreTransitTime?: number; /** * Optional. Default is 15min which is `900`. * Maximum time in seconds for the last street leg. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPostTransitTime?: number; /** * Optional. Default is 30min which is `1800`. * Maximum time in seconds for direct connections. * * If a value smaller than either `maxPreTransitTime` or * `maxPostTransitTime` is used, their maximum is set instead. * Is limited by server config variable `street_routing_max_direct_seconds`. * */ maxDirectTime?: number; /** * If true, the response includes the distance in meters * for each path. This requires path reconstruction and * may be slower than duration-only queries. * * `withDistance` is currently limited to street routing. * */ withDistance?: boolean; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow bike carriage. * */ requireBikeTransport?: boolean; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow car carriage. * */ requireCarTransport?: boolean; }; export type ServerConfig = { /** * the version of this MOTIS server */ motisVersion: string; /** * true if elevation is loaded */ hasElevation: boolean; /** * true if routed transfers available */ hasRoutedTransfers: boolean; /** * true if street routing is available */ hasStreetRouting: boolean; /** * limit for the number of `many` locations for one-to-many requests * */ maxOneToManySize: number; /** * limit for maxTravelTime API param in minutes */ maxOneToAllTravelTimeLimit: number; /** * limit for maxPrePostTransitTime API param in seconds */ maxPrePostTransitTimeLimit: number; /** * limit for maxDirectTime API param in seconds */ maxDirectTimeLimit: number; /** * true if experimental route shapes debug download API is enabled */ shapesDebugEnabled: boolean; }; export type Error = { /** * error message */ error: string; }; /** * Route segment between two stops to show a route on a map */ export type RouteSegment = { /** * Index into the top-level route stops array */ from: number; /** * Index into the top-level route stops array */ to: number; /** * Index into the top-level route polylines array */ polyline: number; }; /** * Shared polyline used by one or more route segments */ export type RoutePolyline = { polyline: EncodedPolyline; /** * Unique route colors of routes containing this segment */ colors: Array<(string)>; /** * Indexes into the top-level routes array for routes containing this segment */ routeIndexes: Array<(number)>; }; export type RouteColor = { color: string; textColor: string; }; export type RoutePathSource = 'NONE' | 'TIMETABLE' | 'ROUTED'; export type TransitRouteInfo = { id: string; shortName: string; longName: string; color?: string; textColor?: string; }; export type RouteInfo = { /** * Transport mode for this route */ mode: Mode; transitRoutes: Array; /** * Number of stops along this route */ numStops: number; /** * Internal route index for debugging purposes */ routeIdx: number; pathSource: RoutePathSource; segments: Array; }; export type PlanData = { query: { /** * Optional. Default is 0 minutes. * * Additional transfer time reserved for each transfer in minutes. * */ additionalTransferTime?: number; /** * algorithm to use */ algorithm?: 'RAPTOR' | 'PONG' | 'TB'; /** * Optional. Default is `false`. * * - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time * - `arriveBy=false`: the parameters `date` and `time` refer to the departure time * */ arriveBy?: boolean; /** * Optional * * Average speed for bike routing. * */ cyclingSpeed?: CyclingSpeed; /** * Controls if `legGeometry` and `steps` are returned for direct legs, * pre-/post-transit legs and transit legs. * */ detailedLegs?: boolean; /** * Controls if transfer polylines and step instructions are returned. * * If not set, this parameter inherits the value of `detailedLegs`. * * - true: Compute transfer polylines and step instructions. * - false: Return empty `legGeometry` and omit `steps` for transfers. * */ detailedTransfers?: boolean; /** * Optional. Default is `WALK` which will compute walking routes as direct connections. * * Modes used for direction connections from start to destination without using transit. * Results will be returned on the `direct` key. * * Note: Direct connections will only be returned on the first call. For paging calls, they can be omitted. * * Note: Transit connections that are slower than the fastest direct connection will not show up. * This is being used as a cut-off during transit routing to speed up the search. * To prevent this, it's possible to send two separate requests (one with only `transitModes` and one with only `directModes`). * * Note: the output `direct` array will stay empty if the input param `maxDirectTime` makes any direct trip impossible. * * Only non-transit modes such as `WALK`, `BIKE`, `CAR`, `BIKE_SHARING`, etc. can be used. * */ directModes?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies to direct connections. * * A list of vehicle type form factors that are allowed to be used for direct connections. * If empty (the default), all form factors are allowed. * Example: `BICYCLE,SCOOTER_STANDING`. * */ directRentalFormFactors?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies to direct connections. * * A list of vehicle type form factors that are allowed to be used for direct connections. * If empty (the default), all propulsion types are allowed. * Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`. * */ directRentalPropulsionTypes?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies to direct connections. * * A list of rental provider groups that are allowed to be used for direct connections. * If empty (the default), all providers are allowed. * */ directRentalProviderGroups?: Array<(string)>; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies to direct connections. * * A list of rental providers that are allowed to be used for direct connections. * If empty (the default), all providers are allowed. * */ directRentalProviders?: Array<(string)>; /** * Optional. Default is `NONE`. * * Set an elevation cost profile, to penalize routes with incline. * - `NONE`: No additional costs for elevations. This is the default behavior * - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. * - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. * * As using an elevation costs profile will increase the travel duration, * routing through steep terrain may exceed the maximal allowed duration, * causing a location to appear unreachable. * Increasing the maximum travel time for these segments may resolve this issue. * * The profile is used for direct routing, on the first mile, and last mile. * * Elevation cost profiles are currently used by following street modes: * - `BIKE` * */ elevationCosts?: ElevationCosts; /** * Optional. Experimental. Default is `1.0`. * Factor with which the duration of the fastest direct non-public-transit connection is multiplied. * Values > 1.0 allow transit connections that are slower than the fastest direct non-public-transit connection to be found. * */ fastestDirectFactor?: number; /** * Optional. * Factor with which the duration of the fastest slowDirect connection is multiplied. * Values > 1.0 allow connections that are slower than the fastest direct transit connection to be found. * Values < 1.0 will return all slowDirect connections. * */ fastestSlowDirectFactor?: number; /** * \`latitude,longitude[,level]\` tuple with * - latitude and longitude in degrees * - (optional) level: the OSM level (default: 0) * * OR * * stop id * */ fromPlace: string; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Default is `false`. * * If set to `true`, the routing will ignore rental return constraints for direct connections, * allowing the rental vehicle to be parked anywhere. * */ ignoreDirectRentalReturnConstraints?: boolean; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Default is `false`. * * If set to `true`, the routing will ignore rental return constraints for the part from the last transit stop to the `to` coordinate, * allowing the rental vehicle to be parked anywhere. * */ ignorePostTransitRentalReturnConstraints?: boolean; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Default is `false`. * * If set to `true`, the routing will ignore rental return constraints for the part from the `from` coordinate to the first transit stop, * allowing the rental vehicle to be parked anywhere. * */ ignorePreTransitRentalReturnConstraints?: boolean; /** * Optional. Default is `true`. * * Controls if a journey section with stay-seated transfers is returned: * - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.). * Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`. * - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops * */ joinInterlinedLegs?: boolean; /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * Optional. Experimental. Number of luggage pieces; base unit: airline cabin luggage (e.g. for ODM or price calculation) * */ luggage?: number; /** * Optional. Default is 30min which is `1800`. * Maximum time in seconds for direct connections. * Is limited by server config variable `street_routing_max_direct_seconds`. * */ maxDirectTime?: number; /** * Optional. By default all computed itineraries will be returned * * The maximum number of itineraries to compute. * This is only relevant if `timetableView=true`. * * Note: With the current implementation, setting this to a lower * number will not result in any speedup. * * Note: The number of returned itineraries might be slightly higher * than `maxItineraries` as there might be several itineraries with * the same departure time but different number of transfers. In order * to not miss any itineraries for paging, either none or all * itineraries with the same departure time have to be returned. * */ maxItineraries?: number; /** * Optional. Default is 25 meters. * * Maximum matching distance in meters to match geo coordinates to the street network. * */ maxMatchingDistance?: number; /** * Optional. Default is 15min which is `900`. * Maximum time in seconds for the last street leg. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPostTransitTime?: number; /** * Optional. Default is 15min which is `900`. * Maximum time in seconds for the first street leg. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPreTransitTime?: number; /** * The maximum number of allowed transfers (i.e. interchanges between transit legs, * pre- and postTransit do not count as transfers). * `maxTransfers=0` searches for direct transit connections without any transfers. * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), * send an empty `transitModes` parameter instead. * * If not provided, the routing uses the server-side default value * which is hardcoded and very high to cover all use cases. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the fastest) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * * In plan endpoints before v3, the behavior is off by one, * i.e. `maxTransfers=0` only returns non-transit connections. * */ maxTransfers?: number; /** * The maximum travel time in minutes. * If not provided, the routing to uses the value * hardcoded in the server which is usually quite high. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the least transfers) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * */ maxTravelTime?: number; /** * Optional. Default is 0 minutes. * * Minimum transfer time for each transfer in minutes. * */ minTransferTime?: number; /** * The minimum number of itineraries to compute. * This is only relevant if `timetableView=true`. * The default value is 5. * */ numItineraries?: number; /** * Use the cursor to go to the next "page" of itineraries. * Copy the cursor from the last response and keep the original request as is. * This will enable you to search for itineraries in the next or previous time-window. * */ pageCursor?: string; /** * Optional. Experimental. Number of passengers (e.g. for ODM or price calculation) */ passengers?: number; /** * Optional. Default is `FOOT`. * * Accessibility profile to use for pedestrian routing in transfers * between transit connections, on the first mile, and last mile. * */ pedestrianProfile?: PedestrianProfile; /** * Optional * * Average speed for pedestrian routing. * */ pedestrianSpeed?: PedestrianSpeed; /** * Optional. Default is `WALK`. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`). * * A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`. * */ postTransitModes?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`). * * A list of vehicle type form factors that are allowed to be used from the last transit stop to the `to` coordinate. * If empty (the default), all form factors are allowed. * Example: `BICYCLE,SCOOTER_STANDING`. * */ postTransitRentalFormFactors?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`). * * A list of vehicle propulsion types that are allowed to be used from the last transit stop to the `to` coordinate. * If empty (the default), all propulsion types are allowed. * Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`. * */ postTransitRentalPropulsionTypes?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`). * * A list of rental provider groups that are allowed to be used from the last transit stop to the `to` coordinate. * If empty (the default), all providers are allowed. * */ postTransitRentalProviderGroups?: Array<(string)>; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`). * * A list of rental providers that are allowed to be used from the last transit stop to the `to` coordinate. * If empty (the default), all providers are allowed. * */ postTransitRentalProviders?: Array<(string)>; /** * Optional. Default is `WALK`. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directModes`). * * A list of modes that are allowed to be used from the `from` coordinate to the first transit stop. Example: `WALK,BIKE_SHARING`. * */ preTransitModes?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`). * * A list of vehicle type form factors that are allowed to be used from the `from` coordinate to the first transit stop. * If empty (the default), all form factors are allowed. * Example: `BICYCLE,SCOOTER_STANDING`. * */ preTransitRentalFormFactors?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`). * * A list of vehicle propulsion types that are allowed to be used from the `from` coordinate to the first transit stop. * If empty (the default), all propulsion types are allowed. * Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`. * */ preTransitRentalPropulsionTypes?: Array; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`). * * A list of rental provider groups that are allowed to be used from the `from` coordinate to the first transit stop. * If empty (the default), all providers are allowed. * */ preTransitRentalProviderGroups?: Array<(string)>; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`). * * A list of rental providers that are allowed to be used from the `from` coordinate to the first transit stop. * If empty (the default), all providers are allowed. * */ preTransitRentalProviders?: Array<(string)>; /** * Experimental. Search radius in meters around the `fromPlace` / `toPlace` coordinates. * When set and the place is given as coordinates, all transit stops within * this radius are used as start/end points with zero pre-transit/post-transit time. * Works without OSM/street routing data loaded. * */ radius?: number; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow bike carriage. * */ requireBikeTransport?: boolean; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow car carriage. * */ requireCarTransport?: boolean; /** * Optional. Default is 15 minutes which is `900`. * * The length of the search-window in seconds. Default value 15 minutes. * * - `arriveBy=true`: number of seconds between the earliest departure time and latest departure time * - `arriveBy=false`: number of seconds between the earliest arrival time and the latest arrival time * */ searchWindow?: number; /** * Optional. Experimental. Adds overtaken direct public transit connections. */ slowDirect?: boolean; /** * Optional. Defaults to the current time. * * Departure time ($arriveBy=false) / arrival date ($arriveBy=true), * */ time?: string; /** * Optional. Query timeout in seconds. */ timeout?: number; /** * Optional. Default is `true`. * * Search for the best trip options within a time window. * If true two itineraries are considered optimal * if one is better on arrival time (earliest wins) * and the other is better on departure time (latest wins). * In combination with arriveBy this parameter cover the following use cases: * * `timetable=false` = waiting for the first transit departure/arrival is considered travel time: * - `arriveBy=true`: event (e.g. a meeting) starts at 10:00 am, * compute the best journeys that arrive by that time (maximizes departure time) * - `arriveBy=false`: event (e.g. a meeting) ends at 11:00 am, * compute the best journeys that depart after that time * * `timetable=true` = optimize "later departure" + "earlier arrival" and give all options over a time window: * - `arriveBy=true`: the time window around `date` and `time` refers to the arrival time window * - `arriveBy=false`: the time window around `date` and `time` refers to the departure time window * */ timetableView?: boolean; /** * \`latitude,longitude[,level]\` tuple with * - latitude and longitude in degrees * - (optional) level: the OSM level (default: 0) * * OR * * stop id * */ toPlace: string; /** * Optional. Default is 1.0 * * Factor to multiply minimum required transfer times with. * Values smaller than 1.0 are not supported. * */ transferTimeFactor?: number; /** * Optional. Default is `TRANSIT` which allows all transit modes (no restriction). * Allowed modes for the transit part. If empty, no transit connections will be computed. * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. * */ transitModes?: Array; /** * Optional. Default is `false`. * * Whether to use transfers routed on OpenStreetMap data. * */ useRoutedTransfers?: boolean; /** * List of via stops to visit (only stop IDs, no coordinates allowed for now). * Also see the optional parameter `viaMinimumStay` to set a set a minimum stay duration for each via stop. * */ via?: Array<(string)>; /** * Optional. If not set, the default is `0,0` - no stay required. * * For each `via` stop a minimum stay duration in minutes. * * The value `0` signals that it's allowed to stay in the same trip. * This enables via stays without counting a transfer and can lead * to better connections with less transfers. Transfer connections can * still be found with `viaMinimumStay=0`. * */ viaMinimumStay?: Array<(number)>; /** * Optional. Experimental. If set to true, the response will contain fare information. */ withFares?: boolean; /** * Optional. Include intermediate stops where passengers can not alight/board according to schedule. */ withScheduledSkippedStops?: boolean; }; }; export type PlanResponse = ({ /** * the routing query */ requestParameters: { [key: string]: (string); }; /** * debug statistics */ debugOutput: { [key: string]: (number); }; from: Place; to: Place; /** * Direct trips by `WALK`, `BIKE`, `CAR`, etc. without time-dependency. * The starting time (`arriveBy=false`) / arrival time (`arriveBy=true`) is always the queried `time` parameter (set to \"now\" if not set). * But all `direct` connections are meant to be independent of absolute times. * */ direct: Array; /** * list of itineraries */ itineraries: Array; /** * Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page. * The previous page is a set of itineraries departing BEFORE the first itinerary in the result for a depart after search. When using the default sort order the previous set of itineraries is inserted before the current result. * */ previousPageCursor: string; /** * Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page. * The next page is a set of itineraries departing AFTER the last itinerary in this result. * */ nextPageCursor: string; }); export type PlanError = (Error); export type OneToManyData = { query: { /** * true = many to one * false = one to many * */ arriveBy: boolean; /** * Optional. Default is `NONE`. * * Set an elevation cost profile, to penalize routes with incline. * - `NONE`: No additional costs for elevations. This is the default behavior * - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. * - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. * * As using an elevation costs profile will increase the travel duration, * routing through steep terrain may exceed the maximal allowed duration, * causing a location to appear unreachable. * Increasing the maximum travel time for these segments may resolve this issue. * * Elevation cost profiles are currently used by following street modes: * - `BIKE` * */ elevationCosts?: ElevationCosts; /** * geo locations as latitude;longitude,latitude;longitude,... * * The number of accepted locations is limited by server config variable `onetomany_max_many`. * */ many: Array<(string)>; /** * maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`. */ max: number; /** * maximum matching distance in meters to match geo coordinates to the street network */ maxMatchingDistance: number; /** * routing profile to use (currently supported: \`WALK\`, \`BIKE\`, \`CAR\`) * */ mode: Mode; /** * geo location as latitude;longitude */ one: string; /** * Optional. Default is `false`. * If true, the response includes the distance in meters * for each path. This requires path reconstruction and * is slower than duration-only queries. * */ withDistance?: boolean; }; }; export type OneToManyResponse = (Array); export type OneToManyError = (Error); export type OneToManyPostData = { body: OneToManyParams; }; export type OneToManyPostResponse = (Array); export type OneToManyPostError = (Error); export type OneToManyIntermodalData = { query: { /** * Optional. Default is 0 minutes. * * Additional transfer time reserved for each transfer in minutes. * */ additionalTransferTime?: number; /** * Optional. Defaults to false, i.e. one to many search * * true = many to one * false = one to many * */ arriveBy?: boolean; /** * Optional * * Average speed for bike routing. * */ cyclingSpeed?: CyclingSpeed; /** * Default is `WALK` which will compute walking routes as direct connections. * * Mode used for direction connections from start to destination without using transit. * * Currently supported non-transit modes: \`WALK\`, \`BIKE\`, \`CAR\` * */ directMode?: Mode; /** * Optional. Default is `NONE`. * * Set an elevation cost profile, to penalize routes with incline. * - `NONE`: No additional costs for elevations. This is the default behavior * - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. * - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. * * As using an elevation costs profile will increase the travel duration, * routing through steep terrain may exceed the maximal allowed duration, * causing a location to appear unreachable. * Increasing the maximum travel time for these segments may resolve this issue. * * The profile is used for routing on both the first and last mile. * * Elevation cost profiles are currently used by following street modes: * - `BIKE` * */ elevationCosts?: ElevationCosts; /** * geo locations as latitude;longitude,latitude;longitude,... * * The number of accepted locations is limited by server config variable `onetomany_max_many`. * */ many: Array<(string)>; /** * Optional. Default is 30min which is `1800`. * Maximum time in seconds for direct connections. * * If a value smaller than either `maxPreTransitTime` or * `maxPostTransitTime` is used, their maximum is set instead. * Is limited by server config variable `street_routing_max_direct_seconds`. * */ maxDirectTime?: number; /** * maximum matching distance in meters to match geo coordinates to the street network */ maxMatchingDistance?: number; /** * Optional. Default is 15min which is `900`. * Maximum time in seconds for the last street leg. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPostTransitTime?: number; /** * Optional. Default is 15min which is `900`. * Maximum time in seconds for the first street leg. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPreTransitTime?: number; /** * The maximum number of allowed transfers (i.e. interchanges between transit legs, * pre- and postTransit do not count as transfers). * `maxTransfers=0` searches for direct transit connections without any transfers. * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), * send an empty `transitModes` parameter instead. * * If not provided, the routing uses the server-side default value * which is hardcoded and very high to cover all use cases. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the fastest) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * */ maxTransfers?: number; /** * The maximum travel time in minutes. * If not provided, the routing uses the value * hardcoded in the server which is usually quite high. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the least transfers) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * */ maxTravelTime?: number; /** * Optional. Default is 0 minutes. * * Minimum transfer time for each transfer in minutes. * */ minTransferTime?: number; /** * geo location as latitude;longitude */ one: string; /** * Optional. Default is `FOOT`. * * Accessibility profile to use for pedestrian routing in transfers * between transit connections and the first and last mile respectively. * */ pedestrianProfile?: PedestrianProfile; /** * Optional * * Average speed for pedestrian routing. * */ pedestrianSpeed?: PedestrianSpeed; /** * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). * * A list of modes that are allowed to be used for the last mile, i.e. from the last transit stop to the target coordinates. Example: `WALK,BIKE_SHARING`. * */ postTransitModes?: Array; /** * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`). * * A list of modes that are allowed to be used for the first mile, i.e. from the coordinates to the first transit stop. Example: `WALK,BIKE_SHARING`. * */ preTransitModes?: Array; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow bike carriage. * */ requireBikeTransport?: boolean; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow car carriage. * */ requireCarTransport?: boolean; /** * Optional. Defaults to the current time. * * Departure time ($arriveBy=false) / arrival date ($arriveBy=true), * */ time?: string; /** * Optional. Default is 1.0 * * Factor to multiply minimum required transfer times with. * Values smaller than 1.0 are not supported. * */ transferTimeFactor?: number; /** * Optional. Default is `TRANSIT` which allows all transit modes (no restriction). * Allowed modes for the transit part. If empty, no transit connections will be computed. * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. * */ transitModes?: Array; /** * Optional. Default is `false`. * * Whether to use transfers routed on OpenStreetMap data. * */ useRoutedTransfers?: boolean; /** * Optional. Default is `false`. * If true, the response includes the distance in meters * for each path. This requires path reconstruction and * is slower than duration-only queries. * * `withDistance` is currently limited to street routing. * */ withDistance?: boolean; }; }; export type OneToManyIntermodalResponse2 = (OneToManyIntermodalResponse); export type OneToManyIntermodalError = (Error); export type OneToManyIntermodalPostData = { body: OneToManyIntermodalParams; }; export type OneToManyIntermodalPostResponse = (OneToManyIntermodalResponse); export type OneToManyIntermodalPostError = (Error); export type OneToAllData = { query: { /** * Optional. Default is 0 minutes. * * Additional transfer time reserved for each transfer in minutes. * */ additionalTransferTime?: number; /** * true = all to one, * false = one to all * */ arriveBy?: boolean; /** * Optional * * Average speed for bike routing. * */ cyclingSpeed?: CyclingSpeed; /** * Optional. Default is `NONE`. * * Set an elevation cost profile, to penalize routes with incline. * - `NONE`: No additional costs for elevations. This is the default behavior * - `LOW`: Add a low cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if small detours are required. * - `HIGH`: Add a high cost for increase in elevation and incline along the way. This will prefer routes with less ascent, if larger detours are required. * * As using an elevation costs profile will increase the travel duration, * routing through steep terrain may exceed the maximal allowed duration, * causing a location to appear unreachable. * Increasing the maximum travel time for these segments may resolve this issue. * * The profile is used for routing on both the first and last mile. * * Elevation cost profiles are currently used by following street modes: * - `BIKE` * */ elevationCosts?: ElevationCosts; /** * Optional. Default is 25 meters. * * Maximum matching distance in meters to match geo coordinates to the street network. * */ maxMatchingDistance?: number; /** * Optional. Default is 15min which is `900`. * - `arriveBy=true`: Maximum time in seconds for the street leg at `one` location. * - `arriveBy=false`: Currently not used * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPostTransitTime?: number; /** * Optional. Default is 15min which is `900`. * - `arriveBy=true`: Currently not used * - `arriveBy=false`: Maximum time in seconds for the street leg at `one` location. * Is limited by server config variable `street_routing_max_prepost_transit_seconds`. * */ maxPreTransitTime?: number; /** * The maximum number of allowed transfers (i.e. interchanges between transit legs, * pre- and postTransit do not count as transfers). * `maxTransfers=0` searches for direct transit connections without any transfers. * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.), * send an empty `transitModes` parameter instead. * * If not provided, the routing uses the server-side default value * which is hardcoded and very high to cover all use cases. * * *Warning*: Use with care. Setting this too low can lead to * optimal (e.g. the fastest) journeys not being found. * If this value is too low to reach the destination at all, * it can lead to slow routing performance. * * In plan endpoints before v3, the behavior is off by one, * i.e. `maxTransfers=0` only returns non-transit connections. * */ maxTransfers?: number; /** * The maximum travel time in minutes. Defaults to 90. The limit may be increased by the server administrator using `onetoall_max_travel_minutes` option in `config.yml`. See documentation for details. */ maxTravelTime: number; /** * Optional. Default is 0 minutes. * * Minimum transfer time for each transfer in minutes. * */ minTransferTime?: number; /** * \`latitude,longitude[,level]\` tuple with * - latitude and longitude in degrees * - (optional) level: the OSM level (default: 0) * * OR * * stop id * */ one: string; /** * Optional. Default is `FOOT`. * * Accessibility profile to use for pedestrian routing in transfers * between transit connections and the first and last mile respectively. * */ pedestrianProfile?: PedestrianProfile; /** * Optional * * Average speed for pedestrian routing. * */ pedestrianSpeed?: PedestrianSpeed; /** * Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set: * - `arriveBy=true`: Only applies if the `one` place is a coordinate (not a transit stop). * - `arriveBy=false`: Currently not used * * A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`. * */ postTransitModes?: Array; /** * Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set: * - `arriveBy=true`: Currently not used * - `arriveBy=false`: Only applies if the `one` place is a coordinate (not a transit stop). * * A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`. * */ preTransitModes?: Array; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow bike carriage. * */ requireBikeTransport?: boolean; /** * Optional. Default is `false`. * * If set to `true`, all used transit trips are required to allow car carriage. * */ requireCarTransport?: boolean; /** * Optional. Defaults to the current time. * * Departure time ($arriveBy=false) / arrival date ($arriveBy=true), * */ time?: string; /** * Optional. Default is 1.0 * * Factor to multiply minimum required transfer times with. * Values smaller than 1.0 are not supported. * */ transferTimeFactor?: number; /** * Optional. Default is `TRANSIT` which allows all transit modes (no restriction). * Allowed modes for the transit part. If empty, no transit connections will be computed. * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`. * */ transitModes?: Array; /** * Optional. Default is `false`. * * Whether to use transfers routed on OpenStreetMap data. * */ useRoutedTransfers?: boolean; }; }; export type OneToAllResponse = (Reachable); export type OneToAllError = (Error); export type ReverseGeocodeData = { query: { /** * latitude, longitude in degrees */ place: string; /** * Optional. Default is all types. * * Only return results of the given type. * For example, this can be used to allow only `ADDRESS` and `STOP` results. * */ type?: LocationType; }; }; export type ReverseGeocodeResponse = (Array); export type ReverseGeocodeError = (Error); export type GeocodeData = { query: { /** * language tags as used in OpenStreetMap * (usually ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * Optional. Filter stops by available transport modes. * Defaults to applying no filter. * */ mode?: Array; /** * Optional. Used for biasing results towards the coordinate. * * Format: latitude,longitude in degrees * */ place?: string; /** * Optional. Used for biasing results towards the coordinate. Higher number = higher bias. * */ placeBias?: number; /** * the (potentially partially typed) address to resolve */ text: string; /** * Optional. Default is all types. * * Only return results of the given types. * For example, this can be used to allow only `ADDRESS` and `STOP` results. * */ type?: LocationType; }; }; export type GeocodeResponse = (Array); export type GeocodeError = (Error); export type TripData = { query: { /** * Controls if `legGeometry` is returned for transit legs. * * The default value is `true`. * */ detailedLegs?: boolean; /** * Optional. Default is `true`. * * Controls if a trip with stay-seated transfers is returned: * - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.). * Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`. * - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops * */ joinInterlinedLegs?: boolean; /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * trip identifier (e.g. from an itinerary leg or stop event) */ tripId: string; /** * Optional. Include intermediate stops where passengers can not alight/board according to schedule. */ withScheduledSkippedStops?: boolean; }; }; export type TripResponse = (Itinerary); export type TripError = (Error); export type StoptimesData = { query?: { /** * Optional. Default is `false`. * * - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time * - `arriveBy=false`: the parameters `date` and `time` refer to the departure time * */ arriveBy?: boolean; /** * Anchor coordinate. Format: latitude,longitude pair. * Used as fallback when "stopId" is missing or can't be found. * If both are provided and "stopId" resolves, "stopId" is used. * If "stopId" does not resolve, "center" is used instead. "radius" is * required when querying by "center" (i.e. without a valid "stopId"). * */ center?: string; /** * This parameter will be ignored in case `pageCursor` is set. * * Optional. Default is * - `LATER` for `arriveBy=false` * - `EARLIER` for `arriveBy=true` * * The response will contain the next `n` arrivals / departures * in case `EARLIER` is selected and the previous `n` * arrivals / departures if `LATER` is selected. * */ direction?: 'EARLIER' | 'LATER'; /** * Optional. Default is `false`. * * If set to `true`, only stations that are phyiscally in the radius are considered. * If set to `false`, additionally to the stations in the radius, equivalences with the same name and children are considered. * */ exactRadius?: boolean; /** * Experimental. Expect unannounced breaking changes (without version bumps). * * Optional. Default is `false`. If set to `true`, the following stops are returned * for departures and the previous stops are returned for arrivals. * */ fetchStops?: boolean; /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * Optional. Default is all transit modes. * * Only return arrivals/departures of the given modes. * */ mode?: Array; /** * Minimum number of events to return. If both `n` and `window` * are provided, the API uses whichever returns more events. * */ n?: number; /** * Use the cursor to go to the next "page" of stop times. * Copy the cursor from the last response and keep the original request as is. * This will enable you to search for stop times in the next or previous time-window. * */ pageCursor?: string; /** * Optional. Radius in meters. * * Default is that only stop times of the parent of the stop itself * and all stops with the same name (+ their child stops) are returned. * * If set, all stops at parent stations and their child stops in the specified radius * are returned. * */ radius?: number; /** * stop id of the stop to retrieve departures/arrivals for */ stopId?: string; /** * Optional. Defaults to the current time. * */ time?: string; /** * Optional. Window in seconds around `time`. * Limiting the response to those that are at most `window` seconds aways in time. * If both `n` and `window` are set, it uses whichever returns more. * */ window?: number; /** * Optional. Default is `true`. If set to `false`, alerts are omitted in the metadata of place for all stopTimes. */ withAlerts?: boolean; /** * Optional. Include stoptimes where passengers can not alight/board according to schedule. */ withScheduledSkippedStops?: boolean; }; }; export type StoptimesResponse = ({ /** * list of stop times */ stopTimes: Array; /** * metadata of the requested stop */ place: Place; /** * Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page. * The previous page is a set of stop times BEFORE the first stop time in the result. * */ previousPageCursor: string; /** * Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page. * The next page is a set of stop times AFTER the last stop time in this result. * */ nextPageCursor: string; }); export type StoptimesError = (Error); export type TripsData = { query: { /** * end if the time window */ endTime: string; /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * latitude,longitude pair of the upper left coordinate */ max: string; /** * latitude,longitude pair of the lower right coordinate */ min: string; /** * precision of returned polylines. Recommended to set based on zoom: `zoom >= 11 ? 5 : zoom >= 8 ? 4 : zoom >= 5 ? 3 : 2` */ precision?: number; /** * start of the time window */ startTime: string; /** * current zoom level */ zoom: number; }; }; export type TripsResponse = (Array); export type TripsError = (Error); export type InitialResponse = ({ /** * latitude */ lat: number; /** * longitude */ lon: number; /** * zoom level */ zoom: number; serverConfig: ServerConfig; }); export type InitialError = (Error); export type StopsData = { query: { /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * latitude,longitude pair of the upper left coordinate */ max: string; /** * latitude,longitude pair of the lower right coordinate */ min: string; }; }; export type StopsResponse = (Array); export type StopsError = (Error); export type LevelsData = { query: { /** * latitude,longitude pair of the upper left coordinate */ max: string; /** * latitude,longitude pair of the lower right coordinate */ min: string; }; }; export type LevelsResponse = (Array<(number)>); export type LevelsError = (Error); export type RoutesData = { query: { /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * latitude,longitude pair of the upper left coordinate */ max: string; /** * latitude,longitude pair of the lower right coordinate */ min: string; /** * current zoom level */ zoom: number; }; }; export type RoutesResponse = ({ routes: Array; polylines: Array; stops: Array; /** * Indicates whether some routes were filtered out due to * the zoom level. * */ zoomFiltered: boolean; }); export type RoutesError = (Error); export type RouteDetailsData = { query: { /** * language tags as used in OpenStreetMap / GTFS * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1) * */ language?: Array<(string)>; /** * Internal route index */ routeIdx: number; }; }; export type RouteDetailsResponse = ({ routes: Array; polylines: Array; stops: Array; /** * Always false for this endpoint. */ zoomFiltered: boolean; }); export type RouteDetailsError = (Error); export type RentalsData = { query?: { /** * latitude,longitude pair of the upper left coordinate */ max?: string; /** * latitude,longitude pair of the lower right coordinate */ min?: string; /** * \`latitude,longitude[,level]\` tuple with * - latitude and longitude in degrees * - (optional) level: the OSM level (ignored, for compatibility reasons) * * OR * * stop id * */ point?: string; /** * A list of rental provider groups to return. * If both `providerGroups` and `providers` are empty/not specified, * all providers in the map section are returned. * */ providerGroups?: Array<(string)>; /** * A list of rental providers to return. * If both `providerGroups` and `providers` are empty/not specified, * all providers in the map section are returned. * */ providers?: Array<(string)>; /** * Radius around `point` in meters. * */ radius?: number; /** * Optional. Include providers in output. If false, only provider * groups are returned. * * Also affects the providers list for each provider group. * */ withProviders?: boolean; /** * Optional. Include stations in output (requires at least min+max or providers filter). */ withStations?: boolean; /** * Optional. Include free-floating vehicles in output (requires at least min+max or providers filter). */ withVehicles?: boolean; /** * Optional. Include geofencing zones in output (requires at least min+max or providers filter). */ withZones?: boolean; }; }; export type RentalsResponse = ({ providerGroups: Array; providers: Array; stations: Array; vehicles: Array; zones: Array; }); export type RentalsError = (Error); export type TransfersData = { query: { /** * location id */ id: string; }; }; export type TransfersResponse = ({ place: Place; root: Place; equivalences: Array; /** * true if the server has foot transfers computed */ hasFootTransfers: boolean; /** * true if the server has wheelchair transfers computed */ hasWheelchairTransfers: boolean; /** * true if the server has car transfers computed */ hasCarTransfers: boolean; /** * all outgoing transfers of this location */ transfers: Array; }); export type TransfersError = unknown; ================================================ FILE: ui/api/package.json ================================================ { "name": "@motis-project/motis-client", "version": "2.0.0", "description": "A JS client for the MOTIS API.", "public": true, "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "/dist" ], "scripts": { "generate": "openapi-ts -i ../../openapi.yaml -o ./openapi/ -c @hey-api/client-fetch", "transpile": "tsup openapi/**/*.ts --format esm --dts -d=./dist", "build": "pnpm generate && pnpm transpile" }, "repository": { "type": "git", "url": "git+https://github.com/motis-project/motis.git" }, "author": "motis-project", "license": "MIT", "bugs": { "url": "https://github.com/motis-project/motis/issues" }, "homepage": "https://github.com/motis-project/motis#readme", "devDependencies": { "@hey-api/openapi-ts": "^0.53.12", "tslib": "^2.8.1", "tsup": "^8.4.0", "typescript": "^5.7.3" }, "dependencies": { "@hey-api/client-fetch": "^0.4.4" }, "type": "module" } ================================================ FILE: ui/api/tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist/" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } ================================================ FILE: ui/components.json ================================================ { "$schema": "https://next.shadcn-svelte.com/schema.json", "style": "new-york", "tailwind": { "config": "tailwind.config.js", "css": "src/app.css", "baseColor": "slate" }, "aliases": { "components": "$lib/components", "utils": "$lib/utils", "ui": "$lib/components/ui", "hooks": "$lib/hooks" }, "typescript": true, "registry": "https://next.shadcn-svelte.com/registry" } ================================================ FILE: ui/eslint.config.js ================================================ import prettier from 'eslint-config-prettier'; import js from '@eslint/js'; import { includeIgnoreFile } from '@eslint/compat'; import svelte from 'eslint-plugin-svelte'; import globals from 'globals'; import { fileURLToPath } from 'node:url'; import ts from 'typescript-eslint'; const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default ts.config( includeIgnoreFile(gitignorePath), js.configs.recommended, ...ts.configs.recommended, ...svelte.configs['flat/recommended'], prettier, ...svelte.configs['flat/prettier'], { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, { files: ['**/*.svelte'], languageOptions: { parserOptions: { parser: ts.parser } } }, { rules: { '@typescript-eslint/no-unused-vars': [ 'error', { args: 'all', argsIgnorePattern: '^_', caughtErrors: 'all', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true } ], 'svelte/no-navigation-without-resolve': 'off' } } ); ================================================ FILE: ui/package.json ================================================ { "name": "ui", "version": "0.0.1", "private": true, "scripts": { "prepare": "svelte-kit sync", "dev": "vite dev", "build": "vite build", "preview": "vite preview", "test": "npm run test:integration && npm run test:unit", "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . && eslint .", "format": "prettier --write .", "test:integration": "playwright test", "test:unit": "vitest", "update-sprite": "spreet sprite --sdf static/sprite_sdf && spreet --sdf --retina sprite static/sprite_sdf@2x", "update-api": "pnpm --filter @motis-project/motis-client build" }, "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@hey-api/openapi-ts": "^0.53.12", "@playwright/test": "^1.58.2", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/earcut": "^3.0.0", "@types/eslint": "^9.6.1", "@types/estree": "^1.0.8", "@types/geojson": "^7946.0.16", "@types/mapbox__polyline": "^1.0.5", "@types/rbush": "^4.0.0", "@typescript-eslint/eslint-plugin": "^8.55.0", "@typescript-eslint/parser": "^8.55.0", "autoprefixer": "^10.4.24", "bits-ui": "^1.8.0", "clsx": "^2.1.1", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.14.0", "globals": "^16.5.0", "postcss": "^8.5.6", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.4.1", "svelte": "^5.50.1", "svelte-check": "^4.3.6", "tailwind-merge": "^2.6.1", "tailwind-variants": "^0.3.1", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.55.0", "vite": "^7.3.1", "vitest": "^4.0.18" }, "type": "module", "dependencies": { "@deck.gl/core": "~9.2.6", "@deck.gl/layers": "~9.2.6", "@deck.gl/mapbox": "~9.2.6", "@hey-api/client-fetch": "^0.4.4", "@lucide/svelte": "^0.548.0", "@mapbox/polyline": "^1.2.1", "@motis-project/motis-client": "workspace:^", "@tanstack/svelte-query": "^6.0.18", "@turf/boolean-point-in-polygon": "^7.3.4", "@turf/circle": "^7.3.4", "@turf/destination": "^7.3.4", "@turf/difference": "^7.3.4", "@turf/helpers": "^7.3.4", "@turf/rhumb-bearing": "^7.3.4", "@turf/rhumb-distance": "^7.3.4", "@turf/union": "^7.3.4", "colord": "^2.9.3", "earcut": "^3.0.2", "geojson": "^0.5.0", "maplibre-gl": "^5.17.0", "rbush": "^4.0.1", "svelte-radix": "^1.1.1" }, "devEngines": { "packageManager": { "name": "pnpm", "onFail": "error" } } } ================================================ FILE: ui/playwright.config.ts ================================================ import type { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { webServer: { command: 'npm run build && npm run preview', port: 4173 }, testDir: 'tests', testMatch: /(.+\.)?(test|spec)\.[jt]s/ }; export default config; ================================================ FILE: ui/pnpm-workspace.yaml ================================================ packages: - api includeWorkspaceRoot: true onlyBuiltDependencies: - esbuild ================================================ FILE: ui/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {} } }; ================================================ FILE: ui/src/app.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 240 10% 3.9%; --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; --card: 0 0% 100%; --card-foreground: 240 10% 3.9%; --border: 240 5.9% 90%; --input: 240 5.9% 90%; --primary: 240 5.9% 10%; --primary-foreground: 0 0% 98%; --secondary: 240 4.8% 95.9%; --secondary-foreground: 240 5.9% 10%; --accent: 240 4.8% 95.9%; --accent-foreground: 240 5.9% 10%; --destructive: 0 72.2% 50.6%; --destructive-foreground: 0 0% 98%; --ring: 240 10% 3.9%; --radius: 0.5rem; --sidebar-background: 0 0% 98%; --sidebar-foreground: 240 5.3% 26.1%; --sidebar-primary: 240 5.9% 10%; --sidebar-primary-foreground: 0 0% 98%; --sidebar-accent: 240 4.8% 95.9%; --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; } .dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; --card: 240 10% 3.9%; --card-foreground: 0 0% 98%; --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --primary: 0 0% 98%; --primary-foreground: 240 5.9% 10%; --secondary: 240 3.7% 15.9%; --secondary-foreground: 0 0% 98%; --accent: 240 3.7% 15.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 45.6%; --destructive-foreground: 0 0% 98%; --ring: 240 4.9% 83.9%; --sidebar-background: 240 5.9% 10%; --sidebar-foreground: 240 4.8% 95.9%; --sidebar-primary: 224.3 76.3% 48%; --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 240 3.7% 15.9%; --sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; } } @layer base { * { @apply border-border; } body { @apply bg-secondary text-foreground; } } body { font: 12px / 20px Helvetica Neue, Arial, Helvetica, sans-serif; } .maplibregl-popup-anchor-top .maplibregl-popup-tip, .maplibregl-popup-anchor-top-left .maplibregl-popup-tip, .maplibregl-popup-anchor-top-right .maplibregl-popup-tip { border-bottom-color: hsl(var(--background)) !important; } .maplibregl-popup-anchor-bottom .maplibregl-popup-tip, .maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip, .maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip { border-top-color: hsl(var(--background)) !important; } .maplibregl-popup-anchor-left .maplibregl-popup-tip { border-right-color: hsl(var(--background)) !important; } .maplibregl-popup-anchor-right .maplibregl-popup-tip { border-left-color: hsl(var(--background)) !important; } .maplibregl-popup-content { background-color: hsl(var(--background)) !important; padding: 12px 40px 12px 12px !important; } .maplibregl-popup-close-button { font-size: 24px !important; width: 32px !important; height: 32px !important; line-height: 32px !important; padding: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; color: hsl(var(--foreground)) !important; opacity: 0.7; transition: opacity 0.2s; } .maplibregl-popup-close-button:hover { background-color: hsl(var(--accent)) !important; opacity: 1; } .maplibregl-control-container .maplibregl-ctrl-top-left { max-width: 100%; bottom: 0.5rem; display: flex; flex-direction: column; z-index: 3; } .maplibregl-ctrl-group .maplibregl-ctrl-geolocate { display: none; } .maplibregl-ctrl-top-left .maplibregl-ctrl.maplibregl-ctrl-scale { margin-top: 25px; } .hide { display: none; } html, body { overscroll-behavior: none; } ================================================ FILE: ui/src/app.d.ts ================================================ // See https://kit.svelte.dev/docs/types#app import type { Itinerary } from '@motis-project/motis-client'; // for information about these interfaces declare global { namespace App { // interface Error {} // interface Locals {} // interface PageData {} interface PageState { selectedItinerary?: Itinerary; selectedStop?: { name: string; stopId: string; time: Date }; stopArriveBy?: boolean; tripId?: string; activeTab?: 'connections' | 'departures' | 'isochrones'; scrollY?: number; } // interface Platform {} } } export {}; ================================================ FILE: ui/src/app.html ================================================ MOTIS %sveltekit.head%
%sveltekit.body%
================================================ FILE: ui/src/index.test.ts ================================================ import { describe, it, expect } from 'vitest'; describe('sum test', () => { it('adds 1 + 2 to equal 3', () => { expect(1 + 2).toBe(3); }); }); ================================================ FILE: ui/src/lib/AddressTypeahead.svelte ================================================ {#snippet modeCircle(mode: Mode)} {@const modeIcon = getModeStyle({ mode } as LegLike)[0]} {@const modeColor = getModeStyle({ mode } as LegLike)[1]}
{/snippet} { if (e) { selected = deserialize(e); inputValue = selected.label!; onChange(selected); } }} > (inputValue = (e.currentTarget as HTMLInputElement).value)} aria-label={placeholder} data-combobox-input={inputValue} /> {#if items.length !== 0} {#each items as item (item.match)}
{#if item.match?.type == 'STOP'} {@render modeCircle(item.match.modes?.length ? item.match.modes![0] : 'BUS')} {:else if item.match?.type == 'ADDRESS'} {:else if item.match?.type == 'PLACE'} {#if !item.match?.category || item.match?.category == 'none'} {:else} {item.match?.category} {/if} {/if}
{item.match?.name} {getDisplayArea(item.match)}
{#if item.match?.type == 'STOP'}
{#each item.match.modes! as mode, i (i)} {@render modeCircle(mode)} {/each}
{/if}
{/each}
{/if}
================================================ FILE: ui/src/lib/AdvancedOptions.svelte ================================================ {#if expanded}
{ if (wheelchair && !checked) { wheelchair = false; } }} /> { if (checked && !useRoutedTransfers) { useRoutedTransfers = true; } }} /> { if (checked && !useRoutedTransfers && allowRoutedTransfers) { useRoutedTransfers = true; } setModes('CAR')(checked); }} />
{t.routingSegments.maxTransfers}
{#if maxTravelTime !== undefined}
{t.routingSegments.maxTravelTime}
{/if}
{#if directModes !== undefined && maxDirectTime !== undefined && ignoreDirectRentalReturnConstraints !== undefined} {/if}
{t.selectElevationCosts}
{t.elevationCosts[elevationCosts]} {#each possibleElevationCosts as costs, i (i + costs.value)} {costs.label} {/each}
{#if additionalComponents} {@render additionalComponents()} {/if}
{/if} ================================================ FILE: ui/src/lib/Alerts.svelte ================================================ {#if alerts.length > 0} {#if variant === 'full'}
{t.alerts.information} {#if alerts.length > 1} +{alerts.length - 1} {t.alerts.more} {/if}
{alerts[0].descriptionText || alerts[0].headerText}
{:else} {/if}
{#each alerts as alert, i (i)}

{alert.headerText}

{#if alert.impactPeriod} {#each alert.impactPeriod as impactPeriod, j (j)} {@const start = new Date(impactPeriod.start ?? 0)} {@const end = new Date(impactPeriod.end ?? 0)}

{t.alerts.validFrom}: {formatDateTime(start, tz)} {t.alerts.until} {formatDateTime(end, tz)} {getTz(start, tz)}

{/each} {/if} {#if alert.causeDetail}

{alert.causeDetail}

{/if} {#if alert.descriptionText}

{alert.descriptionText}

{/if}
{/each}
{/if} ================================================ FILE: ui/src/lib/Color.ts ================================================ export type RGBA = [number, number, number, number]; export function hexToRgb(hex: string): RGBA { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) { throw `${hex} is not a hex color #RRGGBB`; } return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16), 255]; } export function rgbToHex(rgba: RGBA): string { return '#' + ((1 << 24) | (rgba[0] << 16) | (rgba[1] << 8) | rgba[2]).toString(16).slice(1); } export const getDelayColor = (delay: number, realTime: boolean): RGBA => { delay = delay / 60000; if (!realTime) { return [100, 100, 100, 255]; } if (delay <= -30) { return [255, 0, 255, 255]; } else if (delay <= -6) { return [138, 82, 254, 255]; } else if (delay <= 3) { return [69, 194, 74, 255]; } else if (delay <= 5) { return [255, 237, 0, 255]; } else if (delay <= 10) { return [255, 102, 0, 255]; } else if (delay <= 15) { return [255, 48, 71, 255]; } return [163, 0, 10, 255]; }; ================================================ FILE: ui/src/lib/ConnectionDetail.svelte ================================================ {#snippet stopTimes( timestamp: string, scheduledTimestamp: string, isRealtime: boolean, p: Place, mode: Mode, isStartOrEnd: number, hidePlatform?: boolean )} {@const arriveBy = isStartOrEnd == 0 || isStartOrEnd == 1} {@const textColor = isStartOrEnd == 0 ? '' : 'font-semibold'}
{#if p.stopId} {@const pickupNotAllowedOrEnd = p.pickupType == 'NOT_ALLOWED' && isStartOrEnd != 1} {@const dropoffNotAllowedOrStart = p.dropoffType == 'NOT_ALLOWED' && isStartOrEnd != -1}
{#if isStartOrEnd != 0} {/if}
{#if p.track && !hidePlatform} {getModeLabel(mode) == 'Track' ? t.trackAbr : t.platformAbr} {p.track} {/if}
{#if (p as Place & { switchTo?: Leg }).switchTo} {@const switchTo = (p as Place & { switchTo: Leg }).switchTo}
{t.continuesAs} {switchTo.displayName!} {switchTo.headsign}
{/if} {#if pickupNotAllowedOrEnd || dropoffNotAllowedOrStart}
{pickupNotAllowedOrEnd && dropoffNotAllowedOrStart ? t.inOutDisallowed : pickupNotAllowedOrEnd ? t.inDisallowed : t.outDisallowed}
{/if}
{:else} {p.name || p.flex} {/if}
{/snippet} {#snippet streetLeg(l: Leg)} {@const stepsWithElevation = l.steps?.filter( (s: StepInstruction) => s.elevationUp || s.elevationDown )} {@const stepsWithToll = l.steps?.filter((s: StepInstruction) => s.toll)} {@const stepsWithAccessRestriction = l.steps?.filter((s: StepInstruction) => s.accessRestriction)}
{formatDurationSec(l.duration)} {getModeName(l)} {formatDistanceMeters(l.distance)} {#if l.rental && l.rental.systemName} {t.sharingProvider}: {l.rental.systemName} {/if} {#if l.rental?.returnConstraint == 'ROUNDTRIP_STATION'} {t.roundtripStationReturnConstraint} {/if} {#if l.rental?.rentalUriWeb} {/if} {#if stepsWithElevation && stepsWithElevation.length > 0}
{t.incline}
{stepsWithElevation.reduce((acc: number, s: StepInstruction) => acc + s.elevationUp!, 0)} m
{stepsWithElevation.reduce( (acc: number, s: StepInstruction) => acc + s.elevationDown!, 0 )} m
{/if} {#if stepsWithToll && stepsWithToll.length > 0}
{t.toll}
{/if} {#if stepsWithAccessRestriction && stepsWithAccessRestriction.length > 0}
{t.accessRestriction} ({stepsWithAccessRestriction .map((s) => s.accessRestriction) .filter((value, index, array) => array.indexOf(value) === index) .join(', ')})
{/if}
{/snippet} {#snippet productInfo(product: FareProduct)} {product.name} {new Intl.NumberFormat(language, { style: 'currency', currency: product.currency }).format( product.amount )} {#if product.riderCategory} for {#if product.riderCategory.eligibilityUrl} {product.riderCategory.riderCategoryName} {:else} {product.riderCategory.riderCategoryName} {/if} {/if} {#if product.media} as {#if product.media.fareMediaName} {product.media.fareMediaName} {:else} {product.media.fareMediaType} {/if} {/if} {/snippet} {#snippet ticketInfo(prevTransitLeg: Leg | undefined, l: Leg)} {#if itinerary.fareTransfers != undefined && l.fareTransferIndex != undefined && l.effectiveFareLegIndex != undefined} {@const fareTransfer = itinerary.fareTransfers[l.fareTransferIndex]} {@const includedInTransfer = fareTransfer.rule == 'AB' || (fareTransfer.rule == 'A_AB' && l.effectiveFareLegIndex !== 0)} {#if includedInTransfer || fareTransfer.effectiveFareLegProducts[l.effectiveFareLegIndex].length > 0}
{#if includedInTransfer || (prevTransitLeg && prevTransitLeg.fareTransferIndex === l.fareTransferIndex && prevTransitLeg.effectiveFareLegIndex === l.effectiveFareLegIndex)} {t.includedInTicket} {:else} {@const productOptions = fareTransfer.effectiveFareLegProducts[l.effectiveFareLegIndex]} {#if productOptions.length > 1}
{t.ticketOptions}:
{/if}
    1} class:list-outside={productOptions.length > 1} > {#each productOptions as products, i (i)} {#each products as product, j (j)}
  • {@render productInfo(product)}
  • {/each} {/each}
{/if}
{/if} {/if} {/snippet}
{#each itinerary.legs as l, i (i)} {@const isLast = i == itinerary.legs.length - 1} {@const isLastPred = i == itinerary.legs.length - 2} {@const pred = i == 0 ? undefined : itinerary.legs[i - 1]} {@const next = isLast ? undefined : itinerary.legs[i + 1]} {@const prevTransitLeg = itinerary.legs.slice(0, i).find((l) => l.tripId)} {#if l.displayName}
{#if pred && (pred.from.track || isRelevantLeg(pred)) && (i != 1 || pred.displayName)}
{#if pred.duration} {formatDurationSec(pred.duration)} {t.walk} {/if} {#if pred.distance} ({Math.round(pred.distance)} m) {/if} {#if prevTransitLeg?.fareTransferIndex != undefined && itinerary.fareTransfers && itinerary.fareTransfers[prevTransitLeg.fareTransferIndex].transferProducts} {@const transferProducts = itinerary.fareTransfers[prevTransitLeg.fareTransferIndex].transferProducts!} {#if prevTransitLeg.effectiveFareLegIndex === 0 && l.effectiveFareLegIndex === 1}
{#if transferProducts.length > 1}
{t.ticketOptions}:
{/if}
    1} class:list-outside={transferProducts.length > 1} > {#each transferProducts as product, j (j)}
  • {@render productInfo(product)}
  • {/each}
{/if} {/if}
{/if}
{@render stopTimes(l.startTime, l.scheduledStartTime, l.realTime, l.from, l.mode, -1)}
{#if l.tripTo} {:else} {l.headsign} {/if}
{#if l.routeUrl}
{/if} {#if l.loopedCalendarSince}
{t.dataExpiredSince} {formatDate(new Date(l.loopedCalendarSince), l.from.tz)}
{/if} {#if l.cancelled}
{t.tripCancelled}
{/if} {#if !l.scheduled}
{t.unscheduledTrip}
{/if} {#if l.intermediateStops?.length === 0}
{t.tripIntermediateStops(0)} ({formatDurationSec(l.duration)})
{@render ticketInfo(prevTransitLeg, l)} {:else} {@render ticketInfo(prevTransitLeg, l)}
{t.tripIntermediateStops(l.intermediateStops?.length ?? 0)} ({formatDurationSec(l.duration)})
{#each l.intermediateStops! as s, i (i)} {@render stopTimes(s.arrival!, s.scheduledArrival!, l.realTime, s, l.mode, 0)} {/each}
{/if} {#if !isLast && !(isLastPred && !isRelevantLeg(next!))} {@render stopTimes(l.endTime!, l.scheduledEndTime!, l.realTime!, l.to, l.mode, 1)} {/if} {#if isLast || (isLastPred && !isRelevantLeg(next!))}
{/if}
{:else if !(isLast && !isRelevantLeg(l)) && ((i == 0 && isRelevantLeg(l)) || !next || !next.displayName || l.mode != 'WALK' || (pred && (pred.mode == 'BIKE' || (l.mode == 'WALK' && pred.mode == 'CAR') || pred.mode == 'RENTAL')))}
{@render stopTimes(l.startTime, l.scheduledStartTime, l.realTime, l.from, l.mode, -1, true)} {#if l.mode == 'FLEX'}
{formatTime(new Date(l.from.flexStartPickupDropOffWindow!), l.from.tz)} - {formatTime(new Date(l.from.flexEndPickupDropOffWindow!), l.from.tz)}
{/if} {@render streetLeg(l)} {#if !isLast} {@render stopTimes(l.endTime, l.scheduledEndTime, l.realTime, l.to, l.mode, 1, true)} {/if}
{/if} {/each}
{@render stopTimes( lastLeg!.endTime, lastLeg!.scheduledEndTime, lastLeg!.realTime, lastLeg!.to, lastLeg!.mode, 1 )}
================================================ FILE: ui/src/lib/DateInput.svelte ================================================ { // @ts-expect-error target exists, value exists const dateTimeLocalValue = e.target!.value!; const fakeUtcTime = new Date(`${dateTimeLocalValue}Z`); if (!isNaN(fakeUtcTime.getTime())) { /* eslint-disable-next-line svelte/prefer-svelte-reactivity */ value = new Date(fakeUtcTime.getTime() + fakeUtcTime.getTimezoneOffset() * 60000); } }} /> ================================================ FILE: ui/src/lib/Debug.svelte ================================================ {#snippet propertiesTable(_1: maplibregl.MapMouseEvent, _2: () => void, features: any)} {#each Object.entries(features[0].properties) as [key, value], i (i)} {key} {#if key === 'osm_node_id'} {value} {:else if key === 'osm_way_id'} {value} {:else} {value} {/if} {/each}
{/snippet} {#if elevator}

Fahrstuhl {elevator.desc} {elevator.id}

Not available from to {#if elevator.outOfService} {#each elevator.outOfService as _, i (i)} {/each} {/if}
{/if} {#if debug} {#if fps} {#await fps then f} {#if f}

{f.place.name} {f.place.track} {f.place.stopId} Level: {f.place.level}

Station Default {#if f.hasFootTransfers} Foot Foot Routed {/if} {#if f.hasWheelchairTransfers} Wheelchair Wheelchair Routed {/if} {#if f.hasCarTransfers} Car {/if} {#each f.transfers as x, i (i)} {x.to.name}
{x.to.stopId}
{#if x.default !== undefined} {/if} {#if f.hasFootTransfers} {#if x.foot !== undefined} {/if} {#if x.footRouted !== undefined} {/if} {/if} {#if f.hasWheelchairTransfers} {#if x.wheelchair !== undefined} {/if} {#if x.wheelchairRouted !== undefined} {/if} {/if} {#if f.hasCarTransfers} {#if x.car !== undefined} {/if} {/if}
{/each}
{/if} {/await} {/if} {#if matches} {#await matches then m} { const props = e.features![0].properties; id = props.id; }} id="matches" type="circle" filter={['all', ['==', '$type', 'Point']]} layout={{}} paint={{ 'circle-color': ['match', ['get', 'type'], 'location', '#34ebde', '#fa921b'], 'circle-radius': 5 }} > {/await} {/if} {#if flex} {#await flex then f} { const props = e.features![0].properties; id = props.id; }} id="flex-location-groups" type="circle" filter={['all', ['==', '$type', 'Point']]} layout={{}} paint={{ 'circle-color': '#00ff00', 'circle-radius': 5 }} > {/await} {/if} {#if route} {#await route then r} {#if r.type == 'FeatureCollection'} {/if} {/await} {/if} {#if graph != null} {/if} {#if elevators} { // @ts-expect-error type mismatch elevator = parseElevator(e.features![0].properties); }} /> {/if} {#if start} {/if} {#if destination} {/if} {/if} ================================================ FILE: ui/src/lib/DeparturesMask.svelte ================================================
{ if (location.match) { onClickStop(location.label, location.match.id, time); } }} />
================================================ FILE: ui/src/lib/DirectConnection.svelte ================================================ ================================================ FILE: ui/src/lib/ErrorMessage.svelte ================================================

{status} {getErrorType(status ?? 404)}

{message}

================================================ FILE: ui/src/lib/IsochronesInfo.svelte ================================================
{#if options.status == 'WORKING'}
{/if} {#if options.status == 'EMPTY'} {/if} {#if options.status == 'FAILED'} {/if}
================================================ FILE: ui/src/lib/IsochronesMask.svelte ================================================ {#snippet additionalComponents()}
{displayLevels.get(options.displayLevel)} {#each possibleDisplayLevels as level, i (i + level.value)} {level.label} {/each}
{/snippet}
(arriveBy ? 'arrival' : 'departure'), (v) => (arriveBy = v === 'arrival')} onValueChange={swapPrePostData} > pedestrianProfile === 'WHEELCHAIR', (v) => (pedestrianProfile = v ? 'WHEELCHAIR' : 'FOOT') } bind:requireCarTransport bind:requireBikeTransport bind:transitModes bind:maxTransfers bind:maxTravelTime {possibleMaxTravelTimes} bind:preTransitModes bind:postTransitModes directModes={undefined} bind:maxPreTransitTime bind:maxPostTransitTime maxDirectTime={undefined} bind:elevationCosts bind:ignorePreTransitRentalReturnConstraints bind:ignorePostTransitRentalReturnConstraints ignoreDirectRentalReturnConstraints={undefined} {additionalComponents} bind:preTransitProviderGroups bind:postTransitProviderGroups bind:directProviderGroups via={undefined} viaMinimumStay={undefined} viaLabels={{}} {hasDebug} />
================================================ FILE: ui/src/lib/ItineraryList.svelte ================================================ {#snippet legSummary(l: Leg)}
{#if l.displayName} {l.displayName} {:else} {formatDurationSec(l.duration)} {/if}
{/snippet} {#if baseResponse} {#await baseResponse}
{:then r} {#if r.direct.length !== 0}
{#each r.direct as d, i (i)} { selectItinerary(d); }} /> {/each}
{/if} {#if r.itineraries.length !== 0}
{#each routingResponses as r, rI (rI)} {#await r}
{:then r} {#if rI === 0 && baseQuery}
{/if} {#each r.itineraries as it, i (i)} {/each} {#if rI === routingResponses.length - 1 && baseQuery}
{/if} {:catch e} {/await} {/each}
{:else if r.direct.length === 0} {/if} {:catch e} {/await} {/if} ================================================ FILE: ui/src/lib/LevelSelect.svelte ================================================ {#if availableLevels.length > 1} {#each availableLevels as l (l)} {/each} {/if} ================================================ FILE: ui/src/lib/Location.ts ================================================ import maplibregl from 'maplibre-gl'; import type { Match } from '@motis-project/motis-client'; const COORD_LVL_REGEX = /^([+-]?\d+(\.\d+)?)\s*,\s*([+-]?\d+(\.\d+)?)\s*,\s*([+-]?\d+(\.\d+)?)$/; const COORD_REGEX = /^([+-]?\d+(\.\d+)?)\s*,\s*([+-]?\d+(\.\d+)?)$/; export type Location = { label: string; match?: Match; }; export const parseCoordinatesToLocation = (str?: string): Location | undefined => { if (!str) { return undefined; } const coordinateWithLevel = str.match(COORD_LVL_REGEX); if (coordinateWithLevel) { return posToLocation( [Number(coordinateWithLevel[3]), Number(coordinateWithLevel[1])], Number(coordinateWithLevel[5]) ); } const coordinate = str.match(COORD_REGEX); if (coordinate) { return posToLocation([Number(coordinate[3]), Number(coordinate[1])]); } return undefined; }; export function posToLocation(pos: maplibregl.LngLatLike, level?: number): Location { const { lat, lng } = maplibregl.LngLat.convert(pos); const label = level == undefined ? `${lat},${lng}` : `${lat},${lng},${level}`; return { label, match: { lat, lon: lng, level, id: '', areas: [], type: 'PLACE', name: label, tokens: [], score: 0 } }; } export const parseLocation = ( place?: string | null | undefined, name?: string | null | undefined ): Location => { if (!place || place.trim() === '') { return { label: '', match: undefined }; } const coord = parseCoordinatesToLocation(place); if (coord) { if (name) { coord.label = name; coord.match!.name = name; } return coord; } return { label: name || '', match: { lat: 0.0, lon: 0.0, level: 0.0, id: place, areas: [], type: 'STOP', name: name || '', tokens: [], score: 0 } }; }; ================================================ FILE: ui/src/lib/Modes.ts ================================================ import type { Mode, RentalFormFactor } from '@motis-project/motis-client'; export const prePostDirectModes = [ 'WALK', 'BIKE', 'CAR', 'FLEX', 'CAR_DROPOFF', 'CAR_PARKING', 'RENTAL_BICYCLE', 'RENTAL_CARGO_BICYCLE', 'RENTAL_CAR', 'RENTAL_MOPED', 'RENTAL_SCOOTER_STANDING', 'RENTAL_SCOOTER_SEATED', 'RENTAL_OTHER', 'DEBUG_BUS_ROUTE', 'DEBUG_RAILWAY_ROUTE', 'DEBUG_FERRY_ROUTE' ] as const; export type PrePostDirectMode = (typeof prePostDirectModes)[number]; export const getPrePostDirectModes = ( modes: Mode[], formFactors: RentalFormFactor[] ): PrePostDirectMode[] => { return modes .filter((mode) => prePostDirectModes.includes(mode as PrePostDirectMode)) .map((mode) => mode as PrePostDirectMode) .concat(formFactors.map((formFactor) => `RENTAL_${formFactor}` as PrePostDirectMode)); }; export const getFormFactors = (modes: PrePostDirectMode[]): RentalFormFactor[] => { return modes .filter((mode) => mode.startsWith('RENTAL_')) .map((mode) => mode.replace('RENTAL_', '')) as RentalFormFactor[]; }; export const prePostModesToModes = (modes: PrePostDirectMode[]): Mode[] => { const rentalMode: Mode[] = modes.some((mode) => mode.startsWith('RENTAL_')) ? ['RENTAL'] : []; const nonRentalModes = modes.filter((mode) => !mode.startsWith('RENTAL_')); return [...nonRentalModes, ...rentalMode].map((mode) => mode as Mode); }; export const possibleTransitModes = [ 'AIRPLANE', 'HIGHSPEED_RAIL', 'LONG_DISTANCE', 'NIGHT_RAIL', 'COACH', 'RIDE_SHARING', 'REGIONAL_RAIL', 'SUBURBAN', 'SUBWAY', 'TRAM', 'BUS', 'FERRY', 'ODM', 'FUNICULAR', 'AERIAL_LIFT', 'OTHER' ]; export type TransitMode = (typeof possibleTransitModes)[number]; ================================================ FILE: ui/src/lib/NumberSelect.svelte ================================================ value.toString(), (v) => (value = parseInt(v))} items={possibleValues} >
{labelFormatter(value)}
{#each possibleValues as option, i (i + option.value)}
{option.label}
{/each}
================================================ FILE: ui/src/lib/Precision.ts ================================================ export const GEOCODER_PRECISION: number = 50; export const ZOOM_LEVEL_PRECISION: Array = [ 9000, // level 0 entire world 8000, // level 1 entire world 7000, // level 2 entire world 6000, // level 3 entire world 5000, // level 4 entire world 4000, // level 5 entire world 3000, // level 6 entire world 2000, // level 7 entire world 1600, // level 8 entire world 800, // level 9 entire world 400, // level 10 entire world 200, // level 11 entire world 100, // level 12 entire world 50, // level 13 Darmstadt 50, // level 14 50, // level 15 50, // level 16 20, // level 17 Herrngarten 10, // level 18 5, // level 19 5, // level 20 5 // level 21 ]; ================================================ FILE: ui/src/lib/RailViz.svelte ================================================ {#if status && status !== 200} trips response status: {status} {/if} ================================================ FILE: ui/src/lib/Route.svelte ================================================ ================================================ FILE: ui/src/lib/SearchMask.svelte ================================================
(arriveBy ? 'arrival' : 'departure'), (v) => (arriveBy = v === 'arrival')} > pedestrianProfile === 'WHEELCHAIR', (v) => (pedestrianProfile = v ? 'WHEELCHAIR' : 'FOOT') } bind:requireCarTransport bind:maxTransfers maxTravelTime={undefined} bind:requireBikeTransport bind:transitModes bind:preTransitModes bind:postTransitModes bind:directModes bind:maxPreTransitTime bind:maxPostTransitTime bind:maxDirectTime bind:elevationCosts bind:ignorePreTransitRentalReturnConstraints bind:ignorePostTransitRentalReturnConstraints bind:ignoreDirectRentalReturnConstraints bind:preTransitProviderGroups bind:postTransitProviderGroups bind:directProviderGroups bind:via bind:viaMinimumStay bind:viaLabels {hasDebug} />
================================================ FILE: ui/src/lib/StopTimes.svelte ================================================
{#each responses as r, rI (rI)} {#await r}
{:then r} {#if rI === 0 && r.previousPageCursor.length}
{/if} {#each r.stopTimes as stopTime, i (i)} {@const timestamp = arriveBy ? stopTime.place.arrival! : stopTime.place.departure!} {@const scheduledTimestamp = arriveBy ? stopTime.place.scheduledArrival! : stopTime.place.scheduledDeparture!}
{#if stopTime.place.track} {getModeLabel(stopTime.mode) == 'Track' ? t.trackAbr : t.platformAbr} {stopTime.place.track} {/if}
{stopTime.headsign} {#if !stopTime.headsign} {stopTime.tripTo.name} {:else if !stopTime.tripTo.name.startsWith(stopTime.headsign)} ({stopTime.tripTo.name}) {/if}
{#if stopTime.pickupDropoffType == 'NOT_ALLOWED'}
{stopTime.tripCancelled ? t.tripCancelled : stopTime.cancelled ? t.stopCancelled : arriveBy ? t.outDisallowed : t.inDisallowed}
{/if}
{/each} {#if !r.stopTimes.length}
{/if} {#if rI === responses.length - 1 && r.nextPageCursor.length}
{/if} {:catch e}
{/await} {/each} ================================================ FILE: ui/src/lib/StreetModes.svelte ================================================
{label}
{selectedModesLabel} {#each possibleModes as mode, i (i + mode)} {t[mode as TranslationKey]} {/each} maxTransitTime.toString(), (v) => (maxTransitTime = parseInt(v))} > {formatDurationSec(maxTransitTime)} {#each possibleMaxTransitTime as duration (duration)} {formatDurationSec(duration)} {/each}
{t.sharingProviders}
{selectedProviderGroupsLabel} {#each providerGroupOptions as option (option.id)}
{option.name} {#if option.formFactors.length}
{#each Array.from(new Set(option.formFactors.map((ff) => formFactorAssets[ff].svg))).sort() as icon (icon)} {/each}
{/if}
{/each}
!ignoreRentalReturnConstraints, (v) => (ignoreRentalReturnConstraints = !v) } label={t.considerRentalReturnConstraints} id="ignorePreTransitRentalReturnConstraints" />
================================================ FILE: ui/src/lib/Time.svelte ================================================
{#if variant == 'schedule'}
{formatTime(scheduled, timeZone)} {weekday(scheduled)}
{isSameAsBrowserTimezone() ? '' : timeZoneOffset}
{:else if variant === 'realtime-show-always' || (variant === 'realtime' && isRealtime)} {formatTime(t, timeZone)} {weekday(t)} {#if variant === 'realtime-show-always' && !isSameAsBrowserTimezone()}
{isSameAsBrowserTimezone() ? '' : timeZoneOffset}
{/if} {/if}
================================================ FILE: ui/src/lib/TransitModeSelect.svelte ================================================ {selectTransitModesLabel} {#each availableTransitModes as mode, i (i + mode.value)} {mode.label} {/each} ================================================ FILE: ui/src/lib/ViaStopOptions.svelte ================================================
{t.viaStops}
{#each vias as _viaStop, index (index)}
{/each}
================================================ FILE: ui/src/lib/components/ui/button/button.svelte ================================================ {#if href} {@render children?.()} {:else} {/if} ================================================ FILE: ui/src/lib/components/ui/button/index.ts ================================================ import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants, } from "./button.svelte"; export { Root, type ButtonProps as Props, // Root as Button, buttonVariants, type ButtonProps, type ButtonSize, type ButtonVariant, }; ================================================ FILE: ui/src/lib/components/ui/card/card-content.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/card/card-description.svelte ================================================

{@render children?.()}

================================================ FILE: ui/src/lib/components/ui/card/card-footer.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/card/card-header.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/card/card-title.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/card/card.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/card/index.ts ================================================ import Root from "./card.svelte"; import Content from "./card-content.svelte"; import Description from "./card-description.svelte"; import Footer from "./card-footer.svelte"; import Header from "./card-header.svelte"; import Title from "./card-title.svelte"; export { Root, Content, Description, Footer, Header, Title, // Root as Card, Content as CardContent, Description as CardDescription, Footer as CardFooter, Header as CardHeader, Title as CardTitle, }; ================================================ FILE: ui/src/lib/components/ui/dialog/dialog-content.svelte ================================================ {@render children?.()} Close ================================================ FILE: ui/src/lib/components/ui/dialog/dialog-description.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/dialog/dialog-footer.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/dialog/dialog-header.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/dialog/dialog-overlay.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/dialog/dialog-title.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/dialog/index.ts ================================================ import { Dialog as DialogPrimitive } from "bits-ui"; import Title from "./dialog-title.svelte"; import Footer from "./dialog-footer.svelte"; import Header from "./dialog-header.svelte"; import Overlay from "./dialog-overlay.svelte"; import Content from "./dialog-content.svelte"; import Description from "./dialog-description.svelte"; const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root; const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger; const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close; const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal; export { Root, Title, Portal, Footer, Header, Trigger, Overlay, Content, Description, Close, // Root as Dialog, Title as DialogTitle, Portal as DialogPortal, Footer as DialogFooter, Header as DialogHeader, Trigger as DialogTrigger, Overlay as DialogOverlay, Content as DialogContent, Description as DialogDescription, Close as DialogClose, }; ================================================ FILE: ui/src/lib/components/ui/label/index.ts ================================================ import Root from "./label.svelte"; export { Root, // Root as Label, }; ================================================ FILE: ui/src/lib/components/ui/label/label.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/radio-group/index.ts ================================================ import Root from "./radio-group.svelte"; import Item from "./radio-group-item.svelte"; export { Root, Item, // Root as RadioGroup, Item as RadioGroupItem, }; ================================================ FILE: ui/src/lib/components/ui/radio-group/radio-group-item.svelte ================================================ {#snippet children({ checked }: { checked: boolean })}
{#if checked} {/if}
{/snippet}
================================================ FILE: ui/src/lib/components/ui/radio-group/radio-group.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/select/index.ts ================================================ import { Select as SelectPrimitive } from "bits-ui"; import GroupHeading from "./select-group-heading.svelte"; import Item from "./select-item.svelte"; import Content from "./select-content.svelte"; import Trigger from "./select-trigger.svelte"; import Separator from "./select-separator.svelte"; import ScrollDownButton from "./select-scroll-down-button.svelte"; import ScrollUpButton from "./select-scroll-up-button.svelte"; const Root = SelectPrimitive.Root; const Group = SelectPrimitive.Group; export { Root, Item, Group, GroupHeading, Content, Trigger, Separator, ScrollDownButton, ScrollUpButton, // Root as Select, Item as SelectItem, Group as SelectGroup, GroupHeading as SelectGroupHeading, Content as SelectContent, Trigger as SelectTrigger, Separator as SelectSeparator, ScrollDownButton as SelectScrollDownButton, ScrollUpButton as SelectScrollUpButton, }; ================================================ FILE: ui/src/lib/components/ui/select/select-content.svelte ================================================ {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/select/select-group-heading.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/select/select-item.svelte ================================================ {#snippet children({ selected, highlighted }: { selected: boolean; highlighted: boolean })} {#if selected} {/if} {#if childrenProp} {@render childrenProp({ selected, highlighted })} {:else} {label || value} {/if} {/snippet} ================================================ FILE: ui/src/lib/components/ui/select/select-scroll-down-button.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/select/select-scroll-up-button.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/select/select-separator.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/select/select-trigger.svelte ================================================ span]:line-clamp-1', className )} {...restProps} > {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/separator/index.ts ================================================ import Root from "./separator.svelte"; export { Root, // Root as Separator, }; ================================================ FILE: ui/src/lib/components/ui/separator/separator.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/switch/index.ts ================================================ import Root from "./switch.svelte"; export { Root, // Root as Switch, }; ================================================ FILE: ui/src/lib/components/ui/switch/switch.svelte ================================================
{label}
================================================ FILE: ui/src/lib/components/ui/table/index.ts ================================================ import Root from "./table.svelte"; import Body from "./table-body.svelte"; import Caption from "./table-caption.svelte"; import Cell from "./table-cell.svelte"; import Footer from "./table-footer.svelte"; import Head from "./table-head.svelte"; import Header from "./table-header.svelte"; import Row from "./table-row.svelte"; export { Root, Body, Caption, Cell, Footer, Head, Header, Row, // Root as Table, Body as TableBody, Caption as TableCaption, Cell as TableCell, Footer as TableFooter, Head as TableHead, Header as TableHeader, Row as TableRow, }; ================================================ FILE: ui/src/lib/components/ui/table/table-body.svelte ================================================ {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table-caption.svelte ================================================ {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table-cell.svelte ================================================ [role=checkbox]]:translate-y-[2px]", className )} {...restProps} > {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table-footer.svelte ================================================ {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table-head.svelte ================================================ [role=checkbox]]:translate-y-[2px]", className )} {...restProps} > {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table-header.svelte ================================================ {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table-row.svelte ================================================ {@render children?.()} ================================================ FILE: ui/src/lib/components/ui/table/table.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/components/ui/tabs/index.ts ================================================ import { Tabs as TabsPrimitive } from "bits-ui"; import Content from "./tabs-content.svelte"; import List from "./tabs-list.svelte"; import Trigger from "./tabs-trigger.svelte"; const Root = TabsPrimitive.Root; export { Root, Content, List, Trigger, // Root as Tabs, Content as TabsContent, List as TabsList, Trigger as TabsTrigger, }; ================================================ FILE: ui/src/lib/components/ui/tabs/tabs-content.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/tabs/tabs-list.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/tabs/tabs-trigger.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/tabs/tabs.svelte ================================================ ================================================ FILE: ui/src/lib/components/ui/toggle/index.ts ================================================ import Root from "./toggle.svelte"; export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants, } from "./toggle.svelte"; export { Root, // Root as Toggle, }; ================================================ FILE: ui/src/lib/components/ui/toggle/toggle.svelte ================================================ ================================================ FILE: ui/src/lib/constants.ts ================================================ export const LEVEL_MIN_ZOOM = 17; ================================================ FILE: ui/src/lib/defaults.ts ================================================ import type { PlanData } from '@motis-project/motis-client'; export const defaultQuery = { time: undefined, fromPlace: undefined, toPlace: undefined, via: undefined, viaMinimumStay: undefined, arriveBy: false, timetableView: true, withFares: false, searchWindow: 900, pedestrianProfile: 'FOOT', transitModes: ['TRANSIT'], preTransitModes: ['WALK'], postTransitModes: ['WALK'], directModes: ['WALK'], preTransitRentalFormFactors: [], postTransitRentalFormFactors: [], directRentalFormFactors: [], preTransitRentalProviderGroups: [], postTransitRentalProviderGroups: [], directRentalProviderGroups: [], preTransitRentalPropulsionTypes: [], postTransitRentalPropulsionTypes: [], directRentalPropulsionTypes: [], ignorePreTransitRentalReturnConstraints: false, ignorePostTransitRentalReturnConstraints: false, ignoreDirectRentalReturnConstraints: false, requireBikeTransport: false, requireCarTransport: false, elevationCosts: 'NONE', useRoutedTransfers: false, joinInterlinedLegs: true, maxMatchingDistance: 25, maxTransfers: 14, maxTravelTime: 30 * 60, maxPreTransitTime: 900, maxPostTransitTime: 900, maxDirectTime: 1800, fastestDirectFactor: 1.0, additionalTransferTime: 0, transferTimeFactor: 1, numItineraries: 5, circleResolution: undefined, maxItineraries: undefined, passengers: 1, luggage: 0, slowDirect: false, isochronesDisplayLevel: 'GEOMETRY_CIRCLES', isochronesColor: '#ffff00', isochronesOpacity: 250, algorithm: 'PONG' }; export const omitDefaults = (query: PlanData['query']): PlanData['query'] => { const queryCopy: PlanData['query'] = { ...query }; Object.keys(queryCopy).forEach((key) => { if (key in defaultQuery) { const value = queryCopy[key as keyof PlanData['query']]; const defaultValue = defaultQuery[key as keyof typeof defaultQuery]; if (JSON.stringify(value) === JSON.stringify(defaultValue)) { delete queryCopy[key as keyof PlanData['query']]; } } else { console.warn(`Unknown query parameter: ${key}`); } }); return queryCopy; }; ================================================ FILE: ui/src/lib/formatDuration.ts ================================================ export const formatDurationMin = (t: number): string => { let hours = Math.floor(t / 60); let minutes = Math.ceil(t - hours * 60); if (minutes === 60) { hours += 1; minutes = 0; } const str = [ hours !== 0 ? hours + ' h' : '', minutes !== 0 || hours === 0 ? minutes + ' min' : '' ] .join(' ') .trim(); return str; }; export const formatDurationSec = (t: number): string => { let hours = Math.floor(t / 3600); let minutes = Math.ceil((t - hours * 3600) / 60); if (minutes === 60) { hours += 1; minutes = 0; } const str = [ hours !== 0 ? hours + ' h' : '', minutes !== 0 || hours === 0 ? minutes + ' min' : '' ] .join(' ') .trim(); return str; }; export const formatDistanceMeters = (m: number | undefined): string => { if (!m) return ''; const kilometers = Math.floor(m / 1000); const meters = kilometers > 5 ? 0 : Math.ceil(m - kilometers * 1000); const str = [kilometers !== 0 ? kilometers + ' km' : '', meters !== 0 ? meters + ' m' : ''] .join(' ') .trim(); return str; }; ================================================ FILE: ui/src/lib/generateTimes.ts ================================================ export const generateTimes = (limit: number): number[] => { const times: number[] = []; let t = 1; while (t <= limit / 60) { times.push(t * 60); if (t < 5) { t += 4; } else if (t < 30) { t += 5; } else if (t < 60) { t += 10; } else if (t < 120) { t += 30; } else { t += 60; } } if (times[times.length - 1] !== limit) { times.push(limit); } return times; }; ================================================ FILE: ui/src/lib/getModeName.ts ================================================ import type { Leg } from '@motis-project/motis-client'; import { t } from './i18n/translation'; export const getModeName = (l: Leg) => { switch (l.mode) { case 'WALK': return t.walk; case 'BIKE': return t.bike; case 'RENTAL': switch (l.rental?.formFactor) { case 'BICYCLE': return t.bike; case 'CARGO_BICYCLE': return t.cargoBike; case 'CAR': return t.car; case 'MOPED': return t.moped; case 'SCOOTER_SEATED': return t.scooterSeated; case 'SCOOTER_STANDING': return t.scooterStanding; case 'OTHER': default: return t.unknownVehicleType; } case 'CAR': case 'CAR_PARKING': return t.car; case 'ODM': return t.taxi; case 'FLEX': return 'Flex'; default: return `${l.mode}`; } }; ================================================ FILE: ui/src/lib/i18n/bg.ts ================================================ import type { Translations } from './translation'; const translations: Translations = { ticket: 'Билет', ticketOptions: 'Опции за билет', includedInTicket: 'Включено в билета', journeyDetails: 'Детайли за пътуването', transfers: 'прекачвания', walk: 'Пеша', bike: 'Велосипед', cargoBike: 'Товарен велосипед', scooterStanding: 'тротинетка', scooterSeated: 'тротинетка', car: 'Автомобил', taxi: 'Такси', moped: 'Скутер', unknownVehicleType: 'Неизвестен тип превозно средство', electricAssist: 'Електрическа помощ', electric: 'Електрически', combustion: 'Двигател с вътрешно горене', combustionDiesel: 'Дизел', hybrid: 'Хибрид', plugInHybrid: 'Зареждаем хибрид', hydrogenFuelCell: 'Водородна горивна клетка', from: 'От', to: 'До', viaStop: 'Междинна спирка', viaStops: 'Междинни спирки', addViaStop: 'Добави междинна спирка', removeViaStop: 'Премахни междинната спирка', viaStayDuration: 'Минимален престой', position: 'местоположение', arrival: 'Пристигане', departure: 'Заминаване', duration: 'Продължителност', arrivals: 'Пристигания', later: 'по-късно', earlier: 'по-рано', connections: 'Връзки', departures: 'Заминавания', switchToArrivals: 'Превключи към пристигания', switchToDepartures: 'Превключи към заминавания', track: 'Коловоз', platform: 'Перон', trackAbr: 'Кол.', platformAbr: 'Перон', arrivalOnTrack: 'Пристигане на коловоз', tripIntermediateStops: (n: number) => { switch (n) { case 0: return 'Без междинни спирки'; case 1: return '1 междинна спирка'; default: return `${n} междинни спирки`; } }, sharingProvider: 'Оператор', sharingProviders: 'Оператори', returnOnlyAtStations: 'Превозното средство трябва да се върне на станция.', roundtripStationReturnConstraint: 'Превозното средство трябва да се върне на началната станция.', rentalStation: 'Станция', rentalGeofencingZone: 'Зона', noItinerariesFound: 'Не са намерени маршрути.', advancedSearchOptions: 'Разширени настройки', selectTransitModes: 'Изберете видове транспорт', defaultSelectedModes: 'Всички видове транспорт', defaultSelectedProviders: 'Всички превозвачи', selectElevationCosts: 'Избягвай стръмни наклони.', wheelchair: 'инвалидна количка', useRoutedTransfers: 'Използвай посочените прекачвания', bikeRental: 'наемане на велосипед', requireBikeTransport: 'Превоз на велосипед', requireCarTransport: 'Превоз на автомобил', considerRentalReturnConstraints: 'Върни наетите превозни средства в рамките на пътуването', default: 'По подразбиране', timetableSources: 'Източници на разписания', tripCancelled: 'Пътуването е отменено', stopCancelled: 'Спирката е отменена', inOutDisallowed: 'Качване/слизане не е позволено', inDisallowed: 'Качването не е позволено', outDisallowed: 'Слизането не е позволено', unscheduledTrip: 'извънреден курс', alertsAvailable: 'налични известия', dataExpiredSince: 'Данните са остарели, последно валидни на', FLEX: 'По заявка', WALK: 'Пеша', BIKE: 'Велосипед', RENTAL: 'Нает', RIDE_SHARING: 'Споделено пътуване', CAR: 'Автомобил', CAR_DROPOFF: 'Слизане от автомобила', CAR_PARKING: 'Паркинг (P+R)', TRANSIT: 'Градски транспорт', TRAM: 'Трамвай', SUBWAY: 'Метро', FERRY: 'Ферибот', AIRPLANE: 'Самолет', SUBURBAN: 'Градска железница', BUS: 'Автобус', COACH: 'Междуградски автобус', RAIL: 'Влак', HIGHSPEED_RAIL: 'Високоскоростен влак', LONG_DISTANCE: 'Междуградски влак', NIGHT_RAIL: 'Нощен влак', REGIONAL_FAST_RAIL: 'Бърз регионален влак', ODM: 'По заявка', REGIONAL_RAIL: 'Регионален влак', OTHER: 'Други', routingSegments: { maxTransfers: 'Макс. прекачвания', maxTravelTime: 'Макс. време за пътуване', firstMile: 'Първи километър', lastMile: 'Последен километър', direct: 'Директна връзка', maxPreTransitTime: 'Максимално време преди транзит', maxPostTransitTime: 'Максимално време след транзит', maxDirectTime: 'Максимално време без транзит' }, elevationCosts: { NONE: 'Без наклон', LOW: 'Лек наклон', HIGH: 'Голям наклон' }, isochrones: { title: 'Достъпен периметър', displayLevel: 'Ниво на показване', maxComputeLevel: 'Макс. ниво на изчисление', canvasRects: 'Правоъгълници (слой)', canvasCircles: 'Кръгове (слой)', geojsonCircles: 'Кръгове (геометрия)', styling: 'Стил', noData: 'Няма данни', requestFailed: 'Заявката е неуспешна' }, alerts: { validFrom: 'Валидно от', until: 'до', information: 'Информация', more: 'още' }, RENTAL_BICYCLE: 'Споделен велосипед', RENTAL_CARGO_BICYCLE: 'Споделен товарен велосипед', RENTAL_CAR: 'Нает автомобил', RENTAL_MOPED: 'Споделен скутер', RENTAL_SCOOTER_STANDING: 'Споделена тротинетка', RENTAL_SCOOTER_SEATED: 'Споделена седяща тротинетка', RENTAL_OTHER: 'Друго споделено превозно средство', incline: 'Наклон', CABLE_CAR: 'Въжен лифт', FUNICULAR: 'Фуникулер', AERIAL_LIFT: 'Въздушен лифт', toll: 'Внимание! Платен път.', accessRestriction: 'забранен достъп', continuesAs: 'Продължава като', rent: 'Наем', copyToClipboard: 'Копирай в клипборда', rideThroughAllowed: 'Минаване е позволено', rideThroughNotAllowed: 'Минаване не е позволено', rideEndAllowed: 'Паркиране е позволено', rideEndNotAllowed: 'Паркиране не е позволено', DEBUG_BUS_ROUTE: 'Маршрут на автобус (Отстраняване на грешки)', DEBUG_RAILWAY_ROUTE: 'Маршрут на влак (Отстраняване на грешки)', DEBUG_FERRY_ROUTE: 'Маршрут на ферибот (Отстраняване на грешки)', routes: (n: number) => { switch (n) { case 0: return 'Без маршрут'; case 1: return '1 маршрут'; default: return `${n} маршрута`; } } }; export default translations; ================================================ FILE: ui/src/lib/i18n/cs.ts ================================================ import type { Translations } from './translation'; const translations: Translations = { ticket: 'Jízdenka', ticketOptions: 'Možnosti jízdenky', includedInTicket: 'Zahrnuté v jízdence', journeyDetails: 'Detail cesty', transfers: 'přestupy', walk: 'Pěšky', bike: 'Kolo', cargoBike: 'Nákladní kolo', scooterStanding: 'Koloběžka', scooterSeated: 'Koloběžka se sedačkou', car: 'Auto', taxi: 'Taxi', moped: 'Skútr', unknownVehicleType: 'Neznámý typ vozidla', electricAssist: 'Elektrická podpora', electric: 'Elektrické', combustion: 'Spalovací', combustionDiesel: 'Diesel', hybrid: 'Hybridní', plugInHybrid: 'Plug-in hybrid', hydrogenFuelCell: 'Vodíkový palivový článek', from: 'Z', to: 'Do', viaStop: 'Mezizastávka', viaStops: 'Mezizastávky', addViaStop: 'Přidat mezizastávku', removeViaStop: 'Odebrat mezizastávku', viaStayDuration: 'Minimální pobyt', position: 'Pozice', arrival: 'Příjezd', departure: 'Odjezd', duration: 'Čas cesty', arrivals: 'Příjezdy', later: 'později', earlier: 'dřive', departures: 'Odjezdy', connections: 'Spoje', switchToArrivals: 'Přepni na příjezdy', switchToDepartures: 'Přepni na odjezdy', track: 'Kolej', platform: 'Nástupiště', platformAbr: 'Nást.', trackAbr: 'K.', arrivalOnTrack: 'Příjezd na kolej', tripIntermediateStops: (n: number) => { if (n == 0) { return 'Bez mezizastávek'; } if (n == 1) { return '1 mezizastávka'; } if (n % 10 > 1 && n % 10 < 5 && n != 12 && n != 13 && n != 14) { return `${n} mezizastávky`; } return `${n} mezizastávek`; }, sharingProvider: 'Poskytovatel dat', sharingProviders: 'Poskytovatelé dat', returnOnlyAtStations: 'Vozidlo musí být vráceno na stanici.', roundtripStationReturnConstraint: 'Pojezd musí být vrácen k počáteční stanice', rentalStation: 'Stanice', rentalGeofencingZone: 'Zóna', noItinerariesFound: 'Spojení nebylo nalezeno.', advancedSearchOptions: 'Možnosti', selectTransitModes: 'Vyber dopravní prostředky', defaultSelectedModes: 'Všechny dopravní prostředky', defaultSelectedProviders: 'Všichni poskytovatelé', selectElevationCosts: 'Bez prudkého stoupání.', wheelchair: 'Bezbariérové přestupy', useRoutedTransfers: 'Počítej trasu pro přestupy', bikeRental: 'Povol použití sdílených vozidel', requireBikeTransport: 'Přeprava kola', requireCarTransport: 'Přeprava auta', considerRentalReturnConstraints: 'Vrať sdílené vozidla během cesty', default: 'default', timetableSources: 'Zdroje dát JŘ', tripCancelled: 'Spoj odřeknut', stopCancelled: 'Zastávka bez obsluhy', inOutDisallowed: 'Vstup/výstup není povolen', inDisallowed: 'Vstup není povolen', outDisallowed: 'Výstup není povolen', unscheduledTrip: 'Doplňkový spoj', alertsAvailable: 'Oznámení o provozu', dataExpiredSince: 'Pozor: Zastaralá data, platná naposledy', FLEX: 'Poptávková doprava', WALK: 'Chůze', BIKE: 'Kolo', RENTAL: 'Sdílené prostředky', RIDE_SHARING: 'Spolujízda', CAR: 'Auto', CAR_PARKING: 'Auto (využití parkovíšť)', CAR_DROPOFF: 'Auto (pouze zastavení)', TRANSIT: 'Hromadná doprava', TRAM: 'Tramvaj', SUBWAY: 'Metro', FERRY: 'Přívoz', AIRPLANE: 'Letadlo', SUBURBAN: 'Městská železnice', BUS: 'Autobus', COACH: 'Dálkový autokar', RAIL: 'Železnice', HIGHSPEED_RAIL: 'Vysokorychlostní železnice', LONG_DISTANCE: 'Dálková železnice', NIGHT_RAIL: 'Noční vlaky', REGIONAL_FAST_RAIL: 'Zrychlená železnice', ODM: 'Poptávková doprava', REGIONAL_RAIL: 'Regionální železnice', OTHER: 'Jiné', routingSegments: { maxTransfers: 'Max. počet přestupů', maxTravelTime: 'Max. čas cesty', firstMile: 'Přesun k první zastávce', lastMile: 'Přesun od poslední zastávky', direct: 'Přímé spojení', maxPreTransitTime: 'Max. čas přesunu', maxPostTransitTime: 'Max. čas přesunu', maxDirectTime: 'Max. čas přesunu' }, elevationCosts: { NONE: 'Bez odklonů', LOW: 'Malé odklony', HIGH: 'Velké odklony' }, isochrones: { title: 'Izochrony', displayLevel: 'Úroveň ukazování', maxComputeLevel: 'Max. úroveň vypočítání', canvasRects: 'Čtverce', canvasCircles: 'Okruhy (zjednodušená projekce)', geojsonCircles: 'Okruhy (pokročilá projekce)', styling: 'Styl izochron', noData: 'Žádné data', requestFailed: 'Chyba žádosti' }, alerts: { validFrom: 'Platí od', until: 'do', information: 'Informace', more: 'více' }, RENTAL_BICYCLE: 'Sdílené kolo', RENTAL_CARGO_BICYCLE: 'Sdílené nákladní kolo', RENTAL_CAR: 'Sdílené auto', RENTAL_MOPED: 'Sdílený skútr', RENTAL_SCOOTER_STANDING: 'Sdílená koloběžka', RENTAL_SCOOTER_SEATED: 'Sdílená koloběžka se sedačkou', RENTAL_OTHER: 'Jiné sdílené vozidla', incline: 'Sklon', CABLE_CAR: 'Lanová dráha', FUNICULAR: 'Lanová dráha', AERIAL_LIFT: 'Lanová dráha', toll: 'Pozor! Průjezd tuto trasou je placený.', accessRestriction: 'Omezený dostup', continuesAs: 'Pokračuje jako', rent: 'Půjčit si', copyToClipboard: 'Kopírovat do schránky', rideThroughAllowed: 'Průjezd povolen', rideThroughNotAllowed: 'Průjezd zakázán', rideEndAllowed: 'Parkování povoleno', rideEndNotAllowed: 'Parkování pouze na stanicích', DEBUG_BUS_ROUTE: 'Trasa autobusu (Debug)', DEBUG_RAILWAY_ROUTE: 'Trasa vlaku (Debug)', DEBUG_FERRY_ROUTE: 'Trasa trajektu (Debug)', routes: (n: number) => { switch (n) { case 0: return 'Žádná trasa'; case 1: return '1 trasa'; case 2: case 3: case 4: return `${n} trasy`; default: return `${n} tras`; } } }; export default translations; ================================================ FILE: ui/src/lib/i18n/de.ts ================================================ import type { Translations } from './translation'; const translations: Translations = { ticket: 'Fahrschein', ticketOptions: 'Fahrscheinoptionen', includedInTicket: 'Im Fahrschein enthalten', journeyDetails: 'Verbindungsdetails', transfers: 'Umstiege', walk: 'Fußweg', bike: 'Fahrrad', cargoBike: 'Lastenfahrrad', scooterStanding: 'Stehroller', scooterSeated: 'Sitzroller', car: 'Auto', taxi: 'Taxi', moped: 'Moped', unknownVehicleType: 'Unbekannter Fahrzeugtyp', electricAssist: 'Elektromotorunterstützung', electric: 'Elektrisch', combustion: 'Benzin', combustionDiesel: 'Diesel', hybrid: 'Hybrid', plugInHybrid: 'Plug-in Hybrid', hydrogenFuelCell: 'Wasserstoff-Brennstoffzelle', from: 'Von', to: 'Nach', viaStop: 'Zwischenhalt', viaStops: 'Zwischenhalte', addViaStop: 'Zwischenhalt hinzufügen', removeViaStop: 'Zwischenhalt entfernen', viaStayDuration: 'Mindestaufenthalt', position: 'Position', arrival: 'Ankunft', departure: 'Abfahrt', duration: 'Dauer', arrivals: 'Ankünfte', connections: 'Verbindungen', departures: 'Abfahrten', later: 'später', earlier: 'früher', track: 'Gleis', platform: 'Steig', trackAbr: 'Gl.', platformAbr: 'Stg.', arrivalOnTrack: 'Ankunft auf Gleis', switchToArrivals: 'Wechsel zu Ankünften', switchToDepartures: 'Wechsel zu Abfahrten', tripIntermediateStops: (n: number) => { switch (n) { case 0: return 'Fahrt ohne Zwischenhalt'; case 1: return 'Fahrt eine Station'; default: return `Fahrt ${n} Stationen`; } }, sharingProvider: 'Anbieter', sharingProviders: 'Anbieter', returnOnlyAtStations: 'Das Fahrzeug muss an einer Station zurückgegeben werden.', roundtripStationReturnConstraint: 'Das Fahrzeug muss wieder an der Abfahrtsstation abgestellt werden.', rentalStation: 'Station', rentalGeofencingZone: 'Zone', noItinerariesFound: 'Keine Verbindungen gefunden.', advancedSearchOptions: 'Optionen', selectTransitModes: 'Öffentliche Verkehrsmittel auswählen', defaultSelectedModes: 'Alle Verkehrsmittel', defaultSelectedProviders: 'Alle Anbieter', selectElevationCosts: 'Steile Steigungen vermeiden.', useRoutedTransfers: 'Geroutete Umstiege verwenden', wheelchair: 'Barrierefreie Umstiege', bikeRental: 'Sharing-Fahrzeuge berücksichtigen', requireBikeTransport: 'Fahrradmitnahme', requireCarTransport: 'Automitnahme', considerRentalReturnConstraints: 'Leihfahrzeuge innerhalb der Reise zurückgeben', default: 'Vorgabe', timetableSources: 'Fahrplandatenquellen', tripCancelled: 'Fahrt entfällt', stopCancelled: 'Halt entfällt', inOutDisallowed: 'Ein-/Ausstieg nicht möglich', inDisallowed: 'Einstieg nicht möglich', outDisallowed: 'Ausstieg nicht möglich', unscheduledTrip: 'Zusätzliche Fahrt', alertsAvailable: 'Meldungen liegen vor', dataExpiredSince: 'Achtung: Veraltete Daten, zuletzt gültig am', FLEX: 'Bedarfsverkehr', WALK: 'Zu Fuß', BIKE: 'Fahrrad', RENTAL: 'Sharing', RIDE_SHARING: 'Mitfahrgelegenheit', CAR: 'Auto', CAR_DROPOFF: 'Absetzen (Auto)', CAR_PARKING: 'P+R Park & Ride', TRANSIT: 'ÖPV', TRAM: 'Tram', SUBWAY: 'U-Bahn', FERRY: 'Fähre', AIRPLANE: 'Flugzeug', SUBURBAN: 'S-Bahn', BUS: 'Bus', COACH: 'Reisebus', RAIL: 'Zug', HIGHSPEED_RAIL: 'Hochgeschwindigkeitszug', LONG_DISTANCE: 'Intercityzug', NIGHT_RAIL: 'Nachtzug', REGIONAL_FAST_RAIL: 'Schnellzug', ODM: 'Bedarfsverkehr', REGIONAL_RAIL: 'Regionalzug', OTHER: 'Andere', routingSegments: { maxTransfers: 'Max. Umstiege', maxTravelTime: 'Max. Reisezeit', firstMile: 'Erste Meile', lastMile: 'Letzte Meile', direct: 'Direktverbindung', maxPreTransitTime: 'Max. Vorlaufzeit', maxPostTransitTime: 'Max. Nachlaufzeit', maxDirectTime: 'Max. Direktzeit' }, elevationCosts: { NONE: 'Keine Umwege', LOW: 'Kleine Umwege', HIGH: 'Große Umwege' }, isochrones: { title: 'Isochronen', displayLevel: 'Darstellungsebene', maxComputeLevel: 'Max. Berechnungsebene', canvasRects: 'Rechtecke (Overlay)', canvasCircles: 'Kreise (Overlay)', geojsonCircles: 'Kreise (Geometrie)', styling: 'Darstellung der Isochronen', noData: 'Keine Daten', requestFailed: 'Anfrage fehlgeschlagen' }, alerts: { validFrom: 'Gültig von', until: 'bis', information: 'Informationen', more: 'mehr' }, RENTAL_BICYCLE: 'Bikesharing', RENTAL_CARGO_BICYCLE: 'Lastenrad Sharing', RENTAL_CAR: 'Car Sharing', RENTAL_MOPED: 'Moped Sharing', RENTAL_SCOOTER_STANDING: 'Scooter Sharing', RENTAL_SCOOTER_SEATED: 'Sitzroller Sharing', RENTAL_OTHER: 'Anderes sharing Fahrzeug', incline: 'Steigung', CABLE_CAR: 'Seilbahn', FUNICULAR: 'Standseilbahn', AERIAL_LIFT: 'Luftseilbahn', toll: 'Achtung! Mautpflichtige Straße.', accessRestriction: 'Kein Zugang', continuesAs: 'Weiter als', rent: 'Ausleihen', copyToClipboard: 'In die Zwischenablage kopieren', rideThroughAllowed: 'Durchfahrt erlaubt', rideThroughNotAllowed: 'Durchfahrt verboten', rideEndAllowed: 'Parken erlaubt', rideEndNotAllowed: 'Parken nur an Stationen', DEBUG_BUS_ROUTE: 'Busroute (Debug)', DEBUG_RAILWAY_ROUTE: 'Bahnroute (Debug)', DEBUG_FERRY_ROUTE: 'Fährenroute (Debug)', routes: (n: number) => { switch (n) { case 0: return 'Keine Route'; case 1: return '1 Route'; default: return `${n} Routen`; } } }; export default translations; ================================================ FILE: ui/src/lib/i18n/en.ts ================================================ import type { Translations } from './translation'; const translations: Translations = { ticket: 'Ticket', ticketOptions: 'Ticket Options', includedInTicket: 'Included in ticket', journeyDetails: 'Journey Details', transfers: 'transfers', walk: 'Walk', bike: 'Bike', cargoBike: 'Cargo bike', scooterStanding: 'Standing kick scooter', scooterSeated: 'Seated kick scooter', car: 'Car', taxi: 'Taxi', moped: 'Moped', unknownVehicleType: 'Unknown vehicle type', electricAssist: 'Electric motor assist', electric: 'Electric', combustion: 'Combustion', combustionDiesel: 'Diesel', hybrid: 'Hybrid', plugInHybrid: 'Plug-in hybrid', hydrogenFuelCell: 'Hydrogen fuel cell', from: 'From', to: 'To', viaStop: 'Via stop', viaStops: 'Via stops', addViaStop: 'Add via stop', removeViaStop: 'Remove via stop', viaStayDuration: 'Minimum stay', position: 'Position', arrival: 'Arrival', departure: 'Departure', connections: 'Connections', duration: 'Duration', arrivals: 'Arrivals', later: 'later', earlier: 'earlier', departures: 'Departures', switchToArrivals: 'Switch to arrivals', switchToDepartures: 'Switch to departures', track: 'Platform', platform: 'Platform', trackAbr: 'Pl.', platformAbr: 'Pl.', arrivalOnTrack: 'Arrival on track', tripIntermediateStops: (n: number) => { switch (n) { case 0: return 'No intermediate stops'; case 1: return '1 intermediate stop'; default: return `${n} intermediate stops`; } }, sharingProvider: 'Provider', sharingProviders: 'Providers', returnOnlyAtStations: 'The vehicle must be returned at a station.', roundtripStationReturnConstraint: 'The vehicle must be returned to the departure station.', rentalStation: 'Station', rentalGeofencingZone: 'Zone', noItinerariesFound: 'No itineraries found.', advancedSearchOptions: 'Options', selectTransitModes: 'Select transit modes', defaultSelectedModes: 'All transit modes', defaultSelectedProviders: 'All providers', selectElevationCosts: 'Avoid steep incline.', useRoutedTransfers: 'Use routed transfers', wheelchair: 'Accessible transfers', bikeRental: 'Allow usage of sharing vehicles', requireBikeTransport: 'Bike carriage', requireCarTransport: 'Car carriage', considerRentalReturnConstraints: 'Return rental vehicles within journey', default: 'Default', timetableSources: 'Timetable sources', tripCancelled: 'Trip cancelled', stopCancelled: 'Stop cancelled', inOutDisallowed: 'Entry/exit not possible', inDisallowed: 'Entry not possible', outDisallowed: 'Exit not possible', unscheduledTrip: 'Additional service', alertsAvailable: 'Service alerts present', dataExpiredSince: 'Warning: Expired data, last valid', FLEX: 'On-Demand', WALK: 'Walking', BIKE: 'Bike', RENTAL: 'Sharing', RIDE_SHARING: 'Ride sharing', CAR: 'Car', CAR_PARKING: 'Car Parking', CAR_DROPOFF: 'Drop-off (car)', TRANSIT: 'Transit', TRAM: 'Tram', SUBWAY: 'Subway', FERRY: 'Ferry', AIRPLANE: 'Airplane', SUBURBAN: 'Suburban Rail', BUS: 'Bus', COACH: 'Long Distance Bus / Coach', RAIL: 'Train', HIGHSPEED_RAIL: 'High Speed Rail', LONG_DISTANCE: 'Intercity Rail', NIGHT_RAIL: 'Night Rail', REGIONAL_FAST_RAIL: 'Fast Rail', ODM: 'On-Demand Mobility', REGIONAL_RAIL: 'Regional Rail', OTHER: 'Other', RENTAL_BICYCLE: 'Shared bike', RENTAL_CARGO_BICYCLE: 'Shared cargo bike', RENTAL_CAR: 'Shared car', RENTAL_MOPED: 'Shared moped', RENTAL_SCOOTER_STANDING: 'Shared standing scooter', RENTAL_SCOOTER_SEATED: 'Shared seated scooter', RENTAL_OTHER: 'Other shared vehicle', CABLE_CAR: 'Cable car', FUNICULAR: 'Funicular', AERIAL_LIFT: 'Aerial lift', routingSegments: { maxTransfers: 'Max. transfers', maxTravelTime: 'Max. travel time', firstMile: 'First mile', lastMile: 'Last mile', direct: 'Direct connection', maxPreTransitTime: 'Max. pre-transit time', maxPostTransitTime: 'Max. post-transit time', maxDirectTime: 'Max. direct time' }, elevationCosts: { NONE: 'No detours', LOW: 'Small detours', HIGH: 'Large detours' }, isochrones: { title: 'Isochrones', displayLevel: 'Display level', maxComputeLevel: 'Max. computation level', canvasRects: 'Rects (Overlay)', canvasCircles: 'Circles (Overlay)', geojsonCircles: 'Circles (Geometry)', styling: 'Isochrones styling', noData: 'No data', requestFailed: 'Request failed' }, alerts: { validFrom: 'Valid from', until: 'until', information: 'Information', more: 'more' }, incline: 'Incline', toll: 'Warning! A fee must be paid to use this route.', accessRestriction: 'No access', continuesAs: 'Continues as', rent: 'Rent', copyToClipboard: 'Copy to clipboard', rideThroughAllowed: 'Riding through allowed', rideThroughNotAllowed: 'Riding through not allowed', rideEndAllowed: 'Parking allowed', rideEndNotAllowed: 'Parking only at stations', DEBUG_BUS_ROUTE: 'Bus Route (Debug)', DEBUG_RAILWAY_ROUTE: 'Railway Route (Debug)', DEBUG_FERRY_ROUTE: 'Ferry Route (Debug)', routes: (n: number) => { switch (n) { case 0: return 'No routes'; case 1: return '1 route'; default: return `${n} routes`; } } }; export default translations; ================================================ FILE: ui/src/lib/i18n/fr.ts ================================================ import type { Translations } from './translation'; const translations: Translations = { ticket: 'Billet', ticketOptions: 'Options de billet', includedInTicket: 'Inclus dans le billet', journeyDetails: 'Détails du voyage', walk: 'à pied', bike: 'Vélo', cargoBike: 'Vélo Cargo', scooterStanding: 'Trottinette', scooterSeated: 'Trottinette avec siège', car: 'Voiture', taxi: 'Taxi', moped: 'Mobylette', unknownVehicleType: 'Type de véhicule inconnu', electricAssist: 'Assistance moteur électrique', electric: 'Électrique', combustion: 'Combustion', combustionDiesel: 'Diesel', hybrid: 'Hybride', plugInHybrid: 'Hybride rechargeable', hydrogenFuelCell: 'Pile à combustible hydrogène', from: 'De', to: 'À', viaStop: 'Arrêt intermédiaire', viaStops: 'Arrêts intermédiaires', addViaStop: 'Ajouter un arrêt intermédiaire', removeViaStop: "Supprimer l'arrêt intermédiaire", viaStayDuration: "Durée minimale d'arrêt", position: 'Position', arrival: 'Arrivée', departure: 'Départ', duration: 'Durée', arrivals: 'Arrivées', later: 'plus tard', earlier: 'plus tôt', departures: 'Départs', connections: 'Itinéraires', switchToArrivals: 'Afficher les arrivées', switchToDepartures: 'Afficher les départs', track: 'Voie', platform: 'Quai', trackAbr: 'V.', platformAbr: 'Q.', arrivalOnTrack: 'Arrivée sur la voie', tripIntermediateStops: (n: number) => { switch (n) { case 0: return 'Aucun arrêt intermédiaire'; case 1: return '1 arrêt intermédiaire'; default: return `${n} arrêts intermédiaires`; } }, sharingProvider: 'Fournisseur', sharingProviders: 'Fournisseurs', transfers: 'correspondances', returnOnlyAtStations: 'Le véhicule doit être retourné à une station.', roundtripStationReturnConstraint: 'Le véhicule doit être retourné à la station de départ.', rentalStation: 'Station', rentalGeofencingZone: 'Zone', noItinerariesFound: 'Aucun itinéraire trouvé.', advancedSearchOptions: 'Options', selectTransitModes: 'Sélectionner les modes de transport en commun', defaultSelectedModes: 'Tous les transports en commun', defaultSelectedProviders: 'Tous les fournisseurs', selectElevationCosts: 'Évitez les pentes abruptes.', // TODO Online translated wheelchair: 'Correspondances accessibles', useRoutedTransfers: 'Utiliser les correspondances routées', bikeRental: 'Utiliser véhicules partagés', requireBikeTransport: 'Transport vélo', requireCarTransport: 'Transport voiture', considerRentalReturnConstraints: 'Retourner les véhicules partagés pendant le voyage', default: 'Faire défaut', timetableSources: 'Sources des horaires', tripCancelled: 'Voyage annulé', stopCancelled: 'Arrêt supprimé', inOutDisallowed: 'Impossible de monter/descendre', inDisallowed: 'Impossible de monter', outDisallowed: 'Impossible de descendre', unscheduledTrip: 'Voyage supplémentaire', alertsAvailable: 'Annonces disponibles', dataExpiredSince: 'Attention : Données expirées, valides jusqu’au', FLEX: 'Transport à la demande', WALK: 'À pied', BIKE: 'Vélo', RENTAL: 'Loué', RIDE_SHARING: 'Covoiturage', CAR: 'Voiture', CAR_PARKING: 'Garage voiture', CAR_DROPOFF: 'Dépose (voiture)', TRANSIT: 'Transports en commun', TRAM: 'Tram', SUBWAY: 'Métro', FERRY: 'Ferry', AIRPLANE: 'Avion', SUBURBAN: 'RER', BUS: 'Bus', COACH: 'Autocar', RAIL: 'Train', HIGHSPEED_RAIL: 'Train à grande vitesse', LONG_DISTANCE: 'Train intercité', NIGHT_RAIL: 'Train de nuit', REGIONAL_FAST_RAIL: 'Train express', ODM: 'Transport à la demande', REGIONAL_RAIL: 'Train régional', OTHER: 'Autres', routingSegments: { maxTransfers: 'Max. de correspondances', maxTravelTime: 'Temps de trajet max.', firstMile: 'Premier kilomètre', lastMile: 'Dernier kilomètre', direct: 'Connexion directe', maxPreTransitTime: 'Durée max. avant transit', maxPostTransitTime: 'Durée max. après transit', maxDirectTime: 'Durée max. directe' }, elevationCosts: { NONE: 'Pas de détours', LOW: 'Petits détours', HIGH: 'Grands détours' }, isochrones: { title: 'Isochrones', displayLevel: 'Couche de présentation', maxComputeLevel: 'Niveau de calcul max.', canvasRects: 'Rectes (Superposer)', canvasCircles: 'Cercles (Superposer)', geojsonCircles: 'Circles (Géométrie)', styling: 'Style pour Isochrone', noData: 'Pas de données', requestFailed: 'Échec de la demande' }, alerts: { validFrom: 'Valable du', until: 'au', information: 'Informations', more: 'de plus' }, RENTAL_BICYCLE: 'Vélo partagé', RENTAL_CARGO_BICYCLE: 'Vélo cargo partagé', RENTAL_CAR: 'Voiture partagée', RENTAL_MOPED: 'Mobylette partagée', RENTAL_SCOOTER_STANDING: 'Trottinette debout partagée', RENTAL_SCOOTER_SEATED: 'Trottinette assise partagée', RENTAL_OTHER: 'Autre véhicule partagé', incline: 'Pente', CABLE_CAR: 'Téléphérique', FUNICULAR: 'Funiculaire', AERIAL_LIFT: 'Remontée mécanique', toll: 'Attention ! Route à péage.', accessRestriction: 'Accès restreint', continuesAs: 'Continue comme', rent: 'Louer', copyToClipboard: 'Copier dans le presse-papiers', rideThroughAllowed: 'Passage autorisé', rideThroughNotAllowed: 'Passage non autorisé', rideEndAllowed: 'Stationnement autorisé', rideEndNotAllowed: 'Stationnement uniquement aux stations', DEBUG_BUS_ROUTE: 'Itinéraire de bus (Debug)', DEBUG_RAILWAY_ROUTE: 'Itinéraire ferroviaire (Debug)', DEBUG_FERRY_ROUTE: 'Itinéraire de ferry (Debug)', routes: (n: number) => { switch (n) { case 0: return 'Aucun itinéraire'; case 1: return '1 itinéraire'; default: return `${n} itinéraires`; } } }; export default translations; ================================================ FILE: ui/src/lib/i18n/pl.ts ================================================ import type { Translations } from './translation'; const translations: Translations = { ticket: 'Bilet', ticketOptions: 'Opcje biletu', includedInTicket: 'Zawarte w ramach biletu', journeyDetails: 'Szczegóły podróży', transfers: 'przesiadki', walk: 'Pieszo', bike: 'Rower', cargoBike: 'Rower cargo', scooterStanding: 'Hulajnoga stojąca', scooterSeated: 'Hulajnoga z siedziskiem', car: 'Samochód', taxi: 'Taksówka', moped: 'Skuter', unknownVehicleType: 'Nieznany typ pojazdu', electricAssist: 'Wspomaganie elektryczne', electric: 'Elektryczny', combustion: 'Spalinowy', combustionDiesel: 'Diesel', hybrid: 'Hybrydowy', plugInHybrid: 'Hybryda plug-in', hydrogenFuelCell: 'Ogniwo paliwowe na wodór', from: 'Z', to: 'Do', viaStop: 'Przystanek pośredni', viaStops: 'Przystanki pośrednie', addViaStop: 'Dodaj przystanek pośredni', removeViaStop: 'Usuń przystanek pośredni', viaStayDuration: 'Minimalny postój', position: 'Pozycja', arrival: 'Przyjazd', departure: 'Odjazd', duration: 'Czas trwania', arrivals: 'Przyjazdy', later: 'później', earlier: 'wcześniej', departures: 'Odjazdy', connections: 'Połączenia', switchToArrivals: 'Przełącz na przyjazdy', switchToDepartures: 'Przełącz na odjazdy', track: 'Tor', platform: 'Peron', trackAbr: 'T.', platformAbr: 'Pr.', arrivalOnTrack: 'Przyjazd na tor', tripIntermediateStops: (n: number) => { if (n == 0) { return 'Brak przystanków pośrednich'; } if (n == 1) { return '1 przystanek pośredni'; } if (n % 10 > 1 && n % 10 < 5 && n != 12 && n != 13 && n != 14) { return `${n} przystanki pośrednie`; } return `${n} przystanków pośrednich`; }, sharingProvider: 'Dostawca danych', sharingProviders: 'Dostawcy danych', returnOnlyAtStations: 'Pojazd musi zostać zwrócony na stacji.', roundtripStationReturnConstraint: 'Pojazd musi zostać zwrócony do stacji początkowej.', rentalStation: 'Stacja', rentalGeofencingZone: 'Strefa', noItinerariesFound: 'Nie znaleziono połączeń.', advancedSearchOptions: 'Opcje', selectTransitModes: 'Wybierz środki transportu', defaultSelectedModes: 'Wszystkie środki transportu', defaultSelectedProviders: 'Wszyscy dostawcy', selectElevationCosts: 'Unikaj stromych nachyleń.', wheelchair: 'Bezbarierowe przesiadki', useRoutedTransfers: 'Wyznacz trasy dla przesiadek', bikeRental: 'Użyj pojazdów współdzielonych', requireBikeTransport: 'Przewóz roweru', requireCarTransport: 'Przewóz samochodu', considerRentalReturnConstraints: 'Zwróć pojazd współdzielony podczas podróży', default: 'Domyślne', timetableSources: 'Źródła danych rozkładowych', tripCancelled: 'Kurs odwołany', stopCancelled: 'Przystanek nieobsługiwany', inOutDisallowed: 'Zabronione wejście i wyjście', inDisallowed: 'Zabronione wejście', outDisallowed: 'Zabronione wyjście', unscheduledTrip: 'Kurs dodatkowy', alertsAvailable: 'Istnieją ogłoszenia', dataExpiredSince: 'Uwaga: Dane nieaktualne, ostatnio ważne', FLEX: 'Transport na żądanie', WALK: 'Pieszo', BIKE: 'Rower', RENTAL: 'Współdzielenie pojazdów', RIDE_SHARING: 'Wspólne przejazdy', CAR: 'Samochód', CAR_PARKING: 'Samochód (użyj parkingów)', CAR_DROPOFF: 'Samochód (tylko zatrzymanie)', TRANSIT: 'Transport publiczny', TRAM: 'Tramwaj', SUBWAY: 'Metro', FERRY: 'Prom', AIRPLANE: 'Samolot', SUBURBAN: 'Kolej miejska', BUS: 'Autobus', COACH: 'Autokar dalekobieżny', RAIL: 'Kolej', HIGHSPEED_RAIL: 'Kolej dużych prędkości', LONG_DISTANCE: 'Kolej dalekobieżna', NIGHT_RAIL: 'Nocne pociągi', REGIONAL_FAST_RAIL: 'Pociąg pospieszny', ODM: 'Transport na żądanie', REGIONAL_RAIL: 'Kolej regionalna', OTHER: 'Inne', routingSegments: { maxTransfers: 'Maks. ilość przesiadek', maxTravelTime: 'Maks. czas podróży', firstMile: 'Początek podróży', lastMile: 'Osiągnięcie celu', direct: 'Połączenie bezpośrednie', maxPreTransitTime: 'Maks. czas dotarcia', maxPostTransitTime: 'Maks. czas dotarcia', maxDirectTime: 'Maks. czas dotarcia' }, elevationCosts: { NONE: 'Bez odchyleń od trasy', LOW: 'Małe odchylenia od trasy', HIGH: 'Duże odchylenia od trasy' }, isochrones: { title: 'Izochrony', displayLevel: 'Poziom wyświetlania', maxComputeLevel: 'Maks. poziom wyliczenia', canvasRects: 'Kwadraty (warstwa)', canvasCircles: 'Okręgi (warstwa)', geojsonCircles: 'Okręgi (geometria)', styling: 'Styl izochron', noData: 'Brak danych', requestFailed: 'Błąd zapytania' }, alerts: { validFrom: 'Ważne od', until: 'do', information: 'Informacje', more: 'więcej' }, RENTAL_BICYCLE: 'Rower współdzielony', RENTAL_CARGO_BICYCLE: 'Rower cargo współdzielony', RENTAL_CAR: 'Samochód współdzielony', RENTAL_MOPED: 'Skuter współdzielony', RENTAL_SCOOTER_STANDING: 'Hulajnoga stojąca współdzielona', RENTAL_SCOOTER_SEATED: 'Hulajnoga z siedziskiem współdzielona', RENTAL_OTHER: 'Inny pojazd współdzielony', incline: 'Nachylenie', CABLE_CAR: 'Kolej linowa', FUNICULAR: 'Kolej linowo-terenowa', AERIAL_LIFT: 'Wyciąg krzesełkowy', toll: 'Uwaga! Za przejazd tą trasą pobierana jest opłata.', accessRestriction: 'Ograniczony dostęp', continuesAs: 'Kontynuuje jako', rent: 'Wypożycz', copyToClipboard: 'Kopiuj do schowka', rideThroughAllowed: 'Przejazd dozwolony', rideThroughNotAllowed: 'Przejazd niedozwolony', rideEndAllowed: 'Parkowanie dozwolone', rideEndNotAllowed: 'Parkowanie tylko na stacjach', DEBUG_BUS_ROUTE: 'Trasa autobusu (Debug)', DEBUG_RAILWAY_ROUTE: 'Trasa kolejowa (Debug)', DEBUG_FERRY_ROUTE: 'Trasa promu (Debug)', routes: (n: number) => { switch (n) { case 0: return 'Brak trasy'; case 1: return '1 trasa'; default: return `${n} trasy`; } } }; export default translations; ================================================ FILE: ui/src/lib/i18n/translation.ts ================================================ import { browser } from '$app/environment'; import bg from './bg'; import en from './en'; import de from './de'; import fr from './fr'; import pl from './pl'; import cs from './cs'; export type Translations = { ticket: string; ticketOptions: string; includedInTicket: string; journeyDetails: string; transfers: string; walk: string; bike: string; cargoBike: string; scooterStanding: string; scooterSeated: string; car: string; taxi: string; moped: string; unknownVehicleType: string; electricAssist: string; electric: string; combustion: string; combustionDiesel: string; hybrid: string; plugInHybrid: string; hydrogenFuelCell: string; from: string; to: string; viaStop: string; viaStops: string; addViaStop: string; removeViaStop: string; viaStayDuration: string; position: string; arrival: string; departure: string; duration: string; later: string; earlier: string; arrivals: string; departures: string; connections: string; switchToArrivals: string; switchToDepartures: string; arrivalOnTrack: string; track: string; platform: string; trackAbr: string; platformAbr: string; tripIntermediateStops: (n: number) => string; sharingProvider: string; sharingProviders: string; returnOnlyAtStations: string; roundtripStationReturnConstraint: string; rentalStation: string; rentalGeofencingZone: string; noItinerariesFound: string; advancedSearchOptions: string; selectTransitModes: string; defaultSelectedModes: string; defaultSelectedProviders: string; selectElevationCosts: string; wheelchair: string; useRoutedTransfers: string; bikeRental: string; requireBikeTransport: string; requireCarTransport: string; considerRentalReturnConstraints: string; default: string; timetableSources: string; tripCancelled: string; stopCancelled: string; inOutDisallowed: string; inDisallowed: string; outDisallowed: string; unscheduledTrip: string; alertsAvailable: string; dataExpiredSince: string; FLEX: string; WALK: string; BIKE: string; RENTAL: string; CAR: string; CAR_DROPOFF: string; CAR_PARKING: string; TRANSIT: string; TRAM: string; SUBWAY: string; FERRY: string; AIRPLANE: string; SUBURBAN: string; BUS: string; COACH: string; RAIL: string; HIGHSPEED_RAIL: string; LONG_DISTANCE: string; NIGHT_RAIL: string; REGIONAL_FAST_RAIL: string; ODM: string; RIDE_SHARING: string; REGIONAL_RAIL: string; OTHER: string; routingSegments: { maxTransfers: string; maxTravelTime: string; firstMile: string; lastMile: string; direct: string; maxPreTransitTime: string; maxPostTransitTime: string; maxDirectTime: string; }; elevationCosts: { NONE: string; LOW: string; HIGH: string; }; isochrones: { title: string; displayLevel: string; maxComputeLevel: string; canvasRects: string; canvasCircles: string; geojsonCircles: string; styling: string; noData: string; requestFailed: string; }; alerts: { validFrom: string; until: string; information: string; more: string; }; RENTAL_BICYCLE: string; RENTAL_CARGO_BICYCLE: string; RENTAL_CAR: string; RENTAL_MOPED: string; RENTAL_SCOOTER_STANDING: string; RENTAL_SCOOTER_SEATED: string; RENTAL_OTHER: string; incline: string; CABLE_CAR: string; FUNICULAR: string; AERIAL_LIFT: string; toll: string; accessRestriction: string; continuesAs: string; DEBUG_BUS_ROUTE: string; DEBUG_RAILWAY_ROUTE: string; DEBUG_FERRY_ROUTE: string; rent: string; copyToClipboard: string; rideThroughAllowed: string; rideThroughNotAllowed: string; rideEndAllowed: string; rideEndNotAllowed: string; routes: (n: number) => string; }; const translations: Map = new Map( Object.entries({ bg, pl, en, de, fr, cs }) ); const urlLanguage = browser ? new URLSearchParams(window.location.search).get('language') : undefined; const translationsKey = ( urlLanguage && translations.get(urlLanguage ?? '') ? urlLanguage : browser ? (navigator.languages.find((l) => translations.has(l.slice(0, 2))) ?? 'en') : 'en' )?.slice(0, 2); export const language = urlLanguage ?? translationsKey; export const t = translationsKey ? translations.get(translationsKey)! : en; ================================================ FILE: ui/src/lib/lngLatToStr.ts ================================================ import maplibregl from 'maplibre-gl'; export function lngLatToStr(pos: maplibregl.LngLatLike) { const p = maplibregl.LngLat.convert(pos); return `${p.lat},${p.lng}`; } ================================================ FILE: ui/src/lib/map/Control.svelte ================================================
{#if children} {@render children()} {/if}
================================================ FILE: ui/src/lib/map/Drawer.svelte ================================================
{@render children?.()}
================================================ FILE: ui/src/lib/map/GeoJSON.svelte ================================================ {@render children()} ================================================ FILE: ui/src/lib/map/Isochrones.svelte ================================================ ================================================ FILE: ui/src/lib/map/IsochronesShapeWorker.ts ================================================ import { circle } from '@turf/circle'; import { destination } from '@turf/destination'; import { featureCollection, point } from '@turf/helpers'; import { union } from '@turf/union'; import { isLess, type DisplayLevel, type Geometry, type IsochronesPos } from '$lib/map/IsochronesShared'; type LngLat = { lng: number; lat: number; }; type LngLatBounds = { _ne: LngLat; _sw: LngLat; }; export type UpdateMessage = | { level: 'OVERLAY_RECTS'; data: LngLatBounds[] } | { level: 'OVERLAY_CIRCLES'; data: CircleType[] } | { level: 'GEOMETRY_CIRCLES'; data: Geometry | undefined }; export type ShapeMessage = { method: 'update-shape'; index: number } & UpdateMessage; type RectType = { rect: LngLatBounds; distance: number; data: IsochronesPos }; type CircleType = ReturnType; let dataIndex = 0; let data: IsochronesPos[] | undefined = undefined; let rects: RectType[] | undefined = undefined; let circles: CircleType[] | undefined = undefined; let circleGeometry: Geometry | undefined = undefined; let highestComputedLevel: DisplayLevel = 'NONE'; let maxLevel: DisplayLevel = 'NONE'; let working = false; let maxDistance = (_: IsochronesPos) => 0; let circleResolution: number = 64; // default 'steps' for @turf/circle self.onmessage = async function (event) { const method = event.data.method; if (method == 'set-data') { resetState(event.data.index); data = event.data.data; const kilometersPerSecond = event.data.kilometersPerSecond; const maxSeconds = event.data.maxSeconds; maxDistance = getMaxDistanceFunction(kilometersPerSecond, maxSeconds); if (event.data.circleResolution && event.data.circleResolution > 2) { circleResolution = event.data.circleResolution; } } else if (method == 'set-max-level') { maxLevel = event.data.level; createShapes(); } }; function resetState(index: number) { dataIndex = index; rects = undefined; circles = undefined; circleGeometry = undefined; highestComputedLevel = 'NONE'; maxLevel = 'NONE'; maxDistance = (_: IsochronesPos) => 0; } function getMaxDistanceFunction(kilometersPerSecond: number, maxSeconds: number) { return (pos: IsochronesPos) => Math.min(pos.seconds, maxSeconds) * kilometersPerSecond; } async function createShapes() { if (working || !isLess(highestComputedLevel, maxLevel)) { return; } working = true; const workingIndex = dataIndex; const isStale = () => workingIndex != dataIndex; switch (highestComputedLevel) { case 'NONE': await createRects().then(async (allRects: RectType[]) => { if (isStale()) { console.log('Index got stale while computing rects'); return; } rects = allRects; highestComputedLevel = 'OVERLAY_RECTS'; self.postMessage({ method: 'update-shape', index: dataIndex, level: 'OVERLAY_RECTS', data: rects.map((r) => r.rect) } as ShapeMessage); await filterNotContainedRects(allRects).then((notContainedRects: RectType[]) => { if (isStale()) { console.log('Index got stale deleting covered rects'); return; } rects = notContainedRects; self.postMessage({ method: 'update-shape', index: dataIndex, level: 'OVERLAY_RECTS', data: rects.map((r) => r.rect) } as ShapeMessage); }); }); break; case 'OVERLAY_RECTS': await createCircles().then((allCircles: CircleType[]) => { if (isStale()) { console.log('Index got stale while computing circles'); return; } circles = allCircles; highestComputedLevel = 'OVERLAY_CIRCLES'; self.postMessage({ method: 'update-shape', index: dataIndex, level: 'OVERLAY_CIRCLES', data: circles } as ShapeMessage); }); break; case 'OVERLAY_CIRCLES': await createUnion().then((geometry: Geometry | undefined) => { if (isStale()) { console.log('Index got stale while computing geometry'); return; } circleGeometry = geometry; highestComputedLevel = 'GEOMETRY_CIRCLES'; self.postMessage({ method: 'update-shape', index: dataIndex, level: 'GEOMETRY_CIRCLES', data: circleGeometry } as ShapeMessage); }); break; default: console.log(`Unexpected level '${highestComputedLevel}'`); } working = false; createShapes(); } async function createRects() { if (data === undefined) { return []; } const promises = data.map(async (pos: IsochronesPos) => { const center = point([pos.lng, pos.lat]); const r = maxDistance(pos); const north = destination(center, r, 0, { units: 'kilometers' }); const east = destination(center, r, 90, { units: 'kilometers' }); const south = destination(center, r, 180, { units: 'kilometers' }); const west = destination(center, r, -90, { units: 'kilometers' }); return { rect: { _sw: { lng: west.geometry.coordinates[0], lat: south.geometry.coordinates[1] } as LngLat, _ne: { lng: east.geometry.coordinates[0], lat: north.geometry.coordinates[1] } as LngLat } as LngLatBounds, distance: r, data: pos }; }); return await Promise.all(promises); } function contains(larger: RectType, smaller: RectType): boolean { const r1 = larger.rect; const r2 = smaller.rect; return ( r1._sw.lat <= r2._sw.lat && r1._sw.lng <= r2._sw.lng && r1._ne.lat >= r2._ne.lat && r1._ne.lng >= r2._ne.lng ); } async function filterNotContainedRects(allRects: RectType[]) { // Remove all rects, that are completely contained in at least one other // Sort by distance, descending allRects.sort((a: RectType, b: RectType) => b.distance - a.distance); const isCoveredPromises = allRects.map(async (box: RectType, index: number) => allRects.slice(0, index).some((b: RectType) => contains(b, box)) ); const isCovered = await Promise.all(isCoveredPromises); const visibleBoxes = allRects.filter((_: RectType, index: number) => !isCovered[index]); return visibleBoxes; } async function createCircles() { if (rects === undefined) { return []; } const promises = rects.map(async (rect: RectType) => { const c = circle([rect.data.lng, rect.data.lat], rect.distance, { steps: circleResolution, units: 'kilometers' }); // bbox extent in [minX, minY, maxX, maxY] order c.bbox = [rect.rect._sw.lng, rect.rect._sw.lat, rect.rect._ne.lng, rect.rect._ne.lat]; return c; }); return await Promise.all(promises); } // Implementation based on https://stackoverflow.com/a/75982694 // Create union for smaller polygons first // Using a pipe like approach should place larger polygons at the end, // reducing the number of expensive computations async function createUnion() { if (circles === undefined) { return undefined; } const queue: Geometry[] = circles.map((c: CircleType) => c); while (queue.length > 1) { const a: Geometry = queue.shift()!; const b: Geometry = queue.shift()!; const u: Geometry | null = union(featureCollection([a, b])); if (u) { queue.push(u); } } return queue.length == 1 ? queue[0] : undefined; } ================================================ FILE: ui/src/lib/map/IsochronesShared.ts ================================================ import type { Feature, GeoJsonProperties, MultiPolygon, Polygon } from 'geojson'; const DisplayLevels = ['NONE', 'OVERLAY_RECTS', 'OVERLAY_CIRCLES', 'GEOMETRY_CIRCLES'] as const; export type DisplayLevel = (typeof DisplayLevels)[number]; export type StatusLevel = 'WORKING' | 'DONE' | 'EMPTY' | 'FAILED'; export type Geometry = Feature; export interface IsochronesOptions { displayLevel: DisplayLevel; color: string; opacity: number; status: StatusLevel; errorMessage: string | undefined; errorCode: number | undefined; } export interface IsochronesPos { lat: number; lng: number; seconds: number; } export const isLess = (a: DisplayLevel, b: DisplayLevel) => DisplayLevels.indexOf(a) < DisplayLevels.indexOf(b); export const minDisplayLevel = (a: DisplayLevel, b: DisplayLevel) => (isLess(a, b) ? a : b); export const isCanvasLevel = (a: DisplayLevel) => a == 'OVERLAY_RECTS' || a == 'OVERLAY_CIRCLES'; ================================================ FILE: ui/src/lib/map/IsochronesWorker.ts ================================================ import type { circle } from '@turf/circle'; import type { LngLatBounds } from 'maplibre-gl'; import type { Position } from 'geojson'; import type { ShapeMessage, UpdateMessage } from '$lib/map/IsochronesShapeWorker'; import type { DisplayLevel, Geometry } from '$lib/map/IsochronesShared'; import ShapeWorker from '$lib/map/IsochronesShapeWorker.ts?worker'; export type WorkerMessage = { method: 'update-display-level'; level: DisplayLevel; geometry?: Geometry | undefined; index: number; }; type CircleType = ReturnType; let canvas: OffscreenCanvas | undefined = undefined; let dataIndex = 0; let shapeWorker: Worker | undefined = undefined; let rects: LngLatBounds[] | undefined = undefined; let circles: CircleType[] | undefined = undefined; self.onmessage = async function (event) { const method = event.data.method; if (method == 'set-canvas') { canvas = event.data.canvas; } else if (method == 'update-data') { const index: number = event.data.index; const isochronesData = event.data.data; const kilometersPerSecond: number = event.data.kilometersPerSecond; const maxSeconds: number = event.data.maxSeconds; const circleResolution: number = event.data.circleResolution; dataIndex = index; rects = undefined; circles = undefined; const worker = createWorker(); worker.postMessage({ method: 'set-data', index: dataIndex, data: isochronesData, kilometersPerSecond, maxSeconds, circleResolution }); } else if (method == 'set-max-display-level') { if (shapeWorker !== undefined) { const level: DisplayLevel = event.data.displayLevel; shapeWorker.postMessage({ method: 'set-max-level', level: level }); } } else if (method == 'render-canvas') { if (!canvas) { return; } const boundingBox: LngLatBounds = event.data.boundingBox; const color: string = event.data.color; const dimensions: [number, number] = event.data.dimensions; const level: DisplayLevel = event.data.level; canvas.width = dimensions[0]; canvas.height = dimensions[1]; const ctx = canvas.getContext('2d'); if (!ctx) { return; } const transform = getTransformer(boundingBox, dimensions); ctx.fillStyle = color; ctx.clearRect(0, 0, dimensions[0], dimensions[1]); if (level == 'OVERLAY_RECTS') { drawRects(ctx, transform); } else if (level == 'OVERLAY_CIRCLES') { const isVisible = getIsVisible(boundingBox); drawCircles(ctx, transform, isVisible); } else { console.log(`Cannot render level ${level}`); } } }; function getTransformer(boundingBox: LngLatBounds, dimensions: [number, number]) { return (pos: Position) => { const x = Math.round( ((pos[0] - boundingBox._sw.lng) / (boundingBox._ne.lng - boundingBox._sw.lng)) * dimensions[0] ); const y = Math.round( ((boundingBox._ne.lat - pos[1]) / (boundingBox._ne.lat - boundingBox._sw.lat)) * dimensions[1] ); return [x, y]; }; } function getIsVisible(boundingBox: LngLatBounds) { return (circle: CircleType) => { if (!circle.bbox) { return false; } const b = circle.bbox; // [minX, minY, maxX, maxY] return ( boundingBox._sw.lat <= b[3] && b[1] <= boundingBox._ne.lat && boundingBox._sw.lng <= b[2] && b[0] <= boundingBox._ne.lat ); }; } function drawRects(ctx: OffscreenCanvasRenderingContext2D, transform: (p: Position) => Position) { if (rects === undefined) { return; } rects.forEach((rect: LngLatBounds) => { ctx.save(); // Store canvas state const min = transform([rect._sw.lng, rect._sw.lat]); const max = transform([rect._ne.lng, rect._ne.lat]); const diffX = max[0] - min[0]; const diffY = max[1] - min[1]; ctx.fillRect(min[0], min[1], diffX + 1, diffY + 1); // Restore previous state on top ctx.restore(); }); } function drawCircles( ctx: OffscreenCanvasRenderingContext2D, transform: (_: Position) => Position, isVisible: (_: CircleType) => boolean ) { if (circles === undefined) { return; } circles.filter(isVisible).forEach((circle: CircleType) => { ctx.save(); // Store canvas state const b = circle.bbox!; // Existence checked in filter() const min = transform([b[0], b[1]]); const max = transform([b[2], b[3]]); const diffX = max[0] - min[0]; const diffY = max[1] - min[1]; if (diffX < 2 && diffY < 2) { // Draw small rect ctx.fillRect(min[0], min[1], diffX + 1, diffY + 1); } else { // Clip circle ctx.beginPath(); const coords = circle.geometry.coordinates[0]; const start = transform(coords[0]); ctx.moveTo(start[0], start[1]); for (let i = 0; i < coords.length; ++i) { const pos = transform(coords[i]); ctx.lineTo(pos[0], pos[1]); } ctx.clip(); // Fill bounding box, clipped to circle ctx.fillRect(min[0], min[1], diffX + 1, diffY + 1); } // Restore previous state on top ctx.restore(); }); } function createWorker() { shapeWorker?.terminate(); shapeWorker = new ShapeWorker(); shapeWorker.onmessage = (event: { data: ShapeMessage }) => { const method = event.data.method; switch (method) { case 'update-shape': { const index = event.data.index; if (index < dataIndex) { console.log(`Got stale index from shape worker (Got ${index}, expected ${dataIndex})`); return; } const msg: UpdateMessage = event.data; switch (msg.level) { case 'OVERLAY_RECTS': rects = msg.data as maplibregl.LngLatBounds[]; self.postMessage({ method: 'update-display-level', index: dataIndex, level: msg.level } as WorkerMessage); break; case 'OVERLAY_CIRCLES': circles = msg.data; self.postMessage({ method: 'update-display-level', index: dataIndex, level: msg.level } as WorkerMessage); break; case 'GEOMETRY_CIRCLES': { const geometry = msg.data; self.postMessage({ method: 'update-display-level', index: dataIndex, level: msg.level, geometry: geometry } as WorkerMessage); } break; default: console.log(`Unknown message '${msg}`); } } break; default: console.log(`Unknown method '${method}'`); } }; return shapeWorker; } ================================================ FILE: ui/src/lib/map/Layer.svelte ================================================ {#if children} {@render children()} {/if} ================================================ FILE: ui/src/lib/map/Map.svelte ================================================
{#if children} {@render children()} {/if}
================================================ FILE: ui/src/lib/map/Marker.svelte ================================================ ================================================ FILE: ui/src/lib/map/Popup.svelte ================================================ {#if popup && event}
{#if children} {@render children(event, close, features)} {/if}
{/if} ================================================ FILE: ui/src/lib/map/colors.ts ================================================ import { colord } from 'colord'; export function getDecorativeColors(baseColor: string) { const outlineColor = colord(baseColor).darken(0.2).toHex(); const tinted = colord(baseColor).isDark() ? colord(baseColor).lighten(0.35) : colord(baseColor).darken(0.35); const chevronColor = tinted.alpha(0.85).toRgbString(); return { outlineColor, chevronColor }; } ================================================ FILE: ui/src/lib/map/createTripIcon.ts ================================================ import { browser } from '$app/environment'; export function createTripIcon(size: number): HTMLCanvasElement | undefined { if (!browser) { return undefined; } const border = (2 / 64) * size; const padding = (size - size / 2) / 2 + border; const innerSize = size - 2 * padding; const mid = size / 2; const rad = innerSize / 3.5; const cv = document.createElement('canvas'); cv.width = size; cv.height = size; const ctx = cv.getContext('2d', { alpha: true }); if (!ctx) { return cv; } ctx.beginPath(); ctx.arc(padding + rad, mid, rad, (1 / 2) * Math.PI, (3 / 2) * Math.PI, false); ctx.bezierCurveTo(padding + rad + rad, mid - rad, size - padding, mid, size - padding, mid); ctx.bezierCurveTo(size - padding, mid, padding + rad + rad, mid + rad, padding + rad, mid + rad); ctx.closePath(); ctx.fillStyle = 'rgba(255, 0, 0, 0.7)'; ctx.fill(); ctx.lineWidth = border; ctx.strokeStyle = 'rgba(120, 120, 120, 1.0)'; ctx.stroke(); return cv; } ================================================ FILE: ui/src/lib/map/getModeLabel.ts ================================================ import type { Mode } from '@motis-project/motis-client'; export const getModeLabel = (mode: Mode): string => { switch (mode) { case 'BUS': case 'FERRY': case 'TRAM': case 'COACH': case 'AIRPLANE': case 'AERIAL_LIFT': return 'Platform'; default: return 'Track'; } }; ================================================ FILE: ui/src/lib/map/handleScroll.ts ================================================ import { page } from '$app/state'; import { replaceState } from '$app/navigation'; export const restoreScroll = (container: HTMLElement) => { const saveScroll = () => { page.state.scrollY = container.scrollTop; replaceState('', page.state); }; const handlePopState = (event: PopStateEvent) => { requestAnimationFrame(() => { container.scrollTop = event.state?.['sveltekit:states']?.scrollY ?? 0; }); }; container.addEventListener('scrollend', saveScroll); window.addEventListener('popstate', handlePopState); return () => { container.removeEventListener('scrollend', saveScroll); window.removeEventListener('popstate', handlePopState); }; }; export const resetScroll = (container: HTMLElement) => { if (page.state.scrollY == undefined) { container.scrollTop = 0; } }; ================================================ FILE: ui/src/lib/map/itineraries/ItineraryGeoJSON.svelte ================================================ {#each layers as layer (layer.id)} {#if !('line-gradient' in layer.paint) && selected} {/if} {/each} { selectItinerary(); } : undefined} paint={{ 'line-color': selected ? ['get', 'color'] : theme == 'dark' ? '#777' : '#bbb', 'line-width': 7.5, 'line-opacity': 1 }} /> {#each layers as layer (layer.id)} {#if 'line-gradient' in layer.paint && selected} {/if} {/each} ================================================ FILE: ui/src/lib/map/itineraries/itineraryLayers.ts ================================================ import { isLowerLevelRoutingFilter, isUpperLevelRoutingFilter, isCurrentLevelRoutingFilter, leadsToLowerLevelRoutingFilter, leadsUpToCurrentLevelRoutingFilter, leadsDownToCurrentLevelRoutingFilter, leadsToUpperLevelRoutingFilter } from './layerFilters'; /// Routing path current level line color. const routingPathFillColor = '#42a5f5'; /// Routing path current level line outline color. const routingPathOutlineColor = '#0077c2'; /// Routing path other level line color. const routingPathOtherLevelFillColor = '#aaaaaa'; /// Routing path other level line outline color. const routingPathOtherLevelOutlineColor = '#555555'; /// Routing path line color. const routingPathWidth = 7; /// Routing path line color. const routingPathOutlineWidth = routingPathWidth + 2; export const layers = [ // Indoor routing - Outline - Current level \\ { id: 'indoor-routing-path-current-outline', type: 'line', filter: isCurrentLevelRoutingFilter, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': routingPathOutlineColor, 'line-width': routingPathOutlineWidth } }, // Indoor routing - Lower level connecting path segments \\ { id: 'indoor-routing-lower-path-down-outline', type: 'line', filter: leadsToLowerLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-width': routingPathOutlineWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathOutlineColor, 1, routingPathOtherLevelOutlineColor ] } }, { id: 'indoor-routing-lower-path-down', type: 'line', filter: leadsToLowerLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-width': routingPathWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathFillColor, 1, routingPathOtherLevelFillColor ] } }, { id: 'indoor-routing-lower-path-up-outline', type: 'line', filter: leadsUpToCurrentLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-width': routingPathOutlineWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathOtherLevelOutlineColor, 1, routingPathOutlineColor ] } }, { id: 'indoor-routing-lower-path-up', type: 'line', filter: leadsUpToCurrentLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-width': routingPathWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathOtherLevelFillColor, 1, routingPathFillColor ] } }, // Indoor routing - Outline - Upper level connecting path segments \\ { id: 'indoor-routing-upper-path-down-outline', type: 'line', filter: leadsDownToCurrentLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-width': routingPathOutlineWidth, // 'line-gradient' must be specified using an expression // with the special 'line-progress' property // the source must have the 'lineMetrics' option set to true // note the line points have to be ordered so it fits (the direction of the line) // because no other expression are supported here 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathOtherLevelOutlineColor, 1, routingPathOutlineColor ] } }, { id: 'indoor-routing-upper-path-up-outline', type: 'line', filter: leadsToUpperLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-width': routingPathOutlineWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathOutlineColor, 1, routingPathOtherLevelOutlineColor ] } }, // Indoor routing - Concealed edges outline - Below current level \\ { id: 'indoor-routing-path-concealed-below-outline', // required, otherwise line-dasharray will scale with metrics type: 'line', filter: isLowerLevelRoutingFilter, layout: { 'line-join': 'round' }, paint: { 'line-color': routingPathOtherLevelOutlineColor, 'line-width': 2, 'line-gap-width': 6, 'line-dasharray': ['literal', [2, 2]] } }, // Indoor routing - Outline - Above current level \\ { id: 'indoor-routing-path-above-outline', type: 'line', filter: isUpperLevelRoutingFilter, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': routingPathOtherLevelOutlineColor, 'line-width': routingPathOutlineWidth } }, // Indoor routing - Fill - Current level \\ { id: 'indoor-routing-path-current', type: 'line', filter: isCurrentLevelRoutingFilter, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': routingPathFillColor, 'line-width': routingPathWidth } }, // Indoor routing - Fill - Upper level connecting path segments \\ { id: 'indoor-routing-upper-path-down', type: 'line', filter: leadsDownToCurrentLevelRoutingFilter, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-width': routingPathWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathOtherLevelFillColor, 1, routingPathFillColor ] } }, { id: 'indoor-routing-upper-path-up', type: 'line', filter: leadsToUpperLevelRoutingFilter, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-width': routingPathWidth, 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, routingPathFillColor, 1, routingPathOtherLevelFillColor ] } }, // Indoor routing - Fill - Above current level \\ { id: 'indoor-routing-path-above', type: 'line', filter: isUpperLevelRoutingFilter, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': routingPathOtherLevelFillColor, 'line-width': routingPathWidth } } ] as const; ================================================ FILE: ui/src/lib/map/itineraries/layerFilters.ts ================================================ // routing layer \\ import type { ExpressionFilterSpecification } from 'maplibre-gl'; export const currentLevel: ExpressionFilterSpecification = ['coalesce', ['get', 'level'], 0]; export const ceilFromLevel: ExpressionFilterSpecification = [ 'coalesce', ['ceil', ['to-number', ['get', 'fromLevel']]], 0 ]; export const ceilToLevel: ExpressionFilterSpecification = [ 'coalesce', ['ceil', ['to-number', ['get', 'toLevel']]], 0 ]; export const floorFromLevel: ExpressionFilterSpecification = [ 'coalesce', ['floor', ['to-number', ['get', 'fromLevel']]], 0 ]; export const floorToLevel: ExpressionFilterSpecification = [ 'coalesce', ['floor', ['to-number', ['get', 'toLevel']]], 0 ]; /// Filter to match all connections that lie on, cross or connect to the current level. export const connectsToCurrentLevelRoutingFilter: ExpressionFilterSpecification = [ 'all', ['<=', ['min', floorToLevel, floorFromLevel], currentLevel], ['>=', ['max', ceilToLevel, ceilFromLevel], currentLevel] ]; /// Filter to match path connections on the current level. export const isCurrentLevelRoutingFilter: ExpressionFilterSpecification = [ 'any', ['all', ['==', ceilFromLevel, currentLevel], ['==', ceilToLevel, currentLevel]], ['all', ['==', floorFromLevel, currentLevel], ['==', floorToLevel, currentLevel]] ]; /// Filter to match path connections on any lower level that do not connect to current level. export const isLowerLevelRoutingFilter: ExpressionFilterSpecification = [ 'any', ['all', ['<', ceilFromLevel, currentLevel], ['<', ceilToLevel, currentLevel]], ['all', ['<', floorFromLevel, currentLevel], ['<', floorToLevel, currentLevel]] ]; /// Filter to match path connections on any upper level that do not connect to current level. export const isUpperLevelRoutingFilter: ExpressionFilterSpecification = [ 'any', ['all', ['>', ceilFromLevel, currentLevel], ['>', ceilToLevel, currentLevel]], ['all', ['>', floorFromLevel, currentLevel], ['>', floorToLevel, currentLevel]] ]; /// Filter to match paths that act as a connection from the current level to the upper level. export const leadsToUpperLevelRoutingFilter: ExpressionFilterSpecification = [ 'all', ['any', ['==', ceilFromLevel, currentLevel], ['==', floorFromLevel, currentLevel]], ['any', ['>', ceilToLevel, currentLevel], ['>', floorToLevel, currentLevel]] ]; /// Filter to match paths that act as a connection from the upper level to the current level. export const leadsDownToCurrentLevelRoutingFilter: ExpressionFilterSpecification = [ 'all', ['any', ['>', ceilFromLevel, currentLevel], ['>', floorFromLevel, currentLevel]], ['any', ['==', ceilToLevel, currentLevel], ['==', floorToLevel, currentLevel]] ]; /// Filter to match paths that act as a connection from the current level to the lower level. export const leadsToLowerLevelRoutingFilter: ExpressionFilterSpecification = [ 'all', ['any', ['==', ceilFromLevel, currentLevel], ['==', floorFromLevel, currentLevel]], ['any', ['<', ceilToLevel, currentLevel], ['<', floorToLevel, currentLevel]] ]; /// Filter to match paths that act as a connection from the lower level to the current level. export const leadsUpToCurrentLevelRoutingFilter: ExpressionFilterSpecification = [ 'all', ['any', ['<', ceilFromLevel, currentLevel], ['<', floorFromLevel, currentLevel]], ['any', ['==', ceilToLevel, currentLevel], ['==', floorToLevel, currentLevel]] ]; // indoor tile layer \\ export const ceilLevel: ExpressionFilterSpecification = [ 'coalesce', ['ceil', ['to-number', ['get', 'level']]], 0 ]; export const floorLevel: ExpressionFilterSpecification = [ 'coalesce', ['floor', ['to-number', ['get', 'level']]], 0 ]; /// Filter to only show element if level matches current level. /// /// This will show features with level 0.5; 0.3; 0.7 on level 0 and on level 1 export const isCurrentLevelFilter: ExpressionFilterSpecification = [ 'any', ['==', ceilLevel, currentLevel], ['==', floorLevel, currentLevel] ]; /// Filter to match **any** level below the current level. export const isLowerLevelFilter: ExpressionFilterSpecification = [ // important that ceil and floor need to be lower 'all', ['<', ceilLevel, currentLevel], ['<', floorLevel, currentLevel] ]; ================================================ FILE: ui/src/lib/map/rentals/Rentals.svelte ================================================ {#if zoneFeatures.features.length > 0} {@const beforeLayerId = vehicleLayerConfigs[0]?.pointLayerId ?? STATION_ICON_LAYER_ID} {/if} {#each vehicleLayerConfigs as config (config.sourceId)} {/each} {#if providerGroupOptions.length > 0}
{#each providerGroupOptions as option (`${option.providerGroupId}::${option.formFactor}`)} {@const active = isSameFilter(displayFilter, option)} {/each}
{/if} ================================================ FILE: ui/src/lib/map/rentals/StationPopup.svelte ================================================
{station.name}
{#if station.address}
{station.address}
{/if}
{t.sharingProvider}: {#if provider.url} {provider.name} {:else} {provider.name} {/if}
{#if vehicleRows.length} {#each vehicleRows as vehicle (vehicle.id)} {/each}
{vehicle.available}x {#if vehicle.propulsionIcon} {@const PropulsionIcon = vehicle.propulsionIcon.component} {/if} {vehicle.name} {#if vehicle.returnIcon} {@const ReturnIcon = vehicle.returnIcon.component} {/if}
{/if} {#if showActions && station.rentalUriWeb} {/if} {#if debug}
{JSON.stringify(debugInfo, null, 2)}
{/if}
================================================ FILE: ui/src/lib/map/rentals/VehiclePopup.svelte ================================================
{#if propulsion} {/if} {vehicleType?.name || formFactorAssets[vehicle.formFactor].label}
{t.sharingProvider}: {#if provider.url} {provider.name} {:else} {provider.name} {/if}
{#if returnConstraint}
{returnConstraint.title}
{/if} {#if showActions && vehicle.rentalUriWeb} {/if} {#if debug}
{JSON.stringify(debugInfo, null, 2)}
{/if}
================================================ FILE: ui/src/lib/map/rentals/ZoneLayer.svelte ================================================ ================================================ FILE: ui/src/lib/map/rentals/ZonePopup.svelte ================================================
{#if zone} {#if zone.name}
{t.rentalGeofencingZone}: {zone.name}
{:else}
{t.rentalGeofencingZone}
{/if} {:else if station} {#if station.name}
{t.rentalStation}: {station.name}
{:else}
{t.rentalStation}
{/if} {/if}
{t.sharingProvider}: {#if provider.url} {provider.name} {:else} {provider.name} {/if}
{rideThroughAllowed ? t.rideThroughAllowed : t.rideThroughNotAllowed}
{#if rideThroughAllowed}
{rideEndAllowed ? t.rideEndAllowed : t.rideEndNotAllowed}
{/if}
{#if debug}
{JSON.stringify(debugInfo, null, 2)}
{/if}
================================================ FILE: ui/src/lib/map/rentals/assets.ts ================================================ import { browser } from '$app/environment'; import type { RentalFormFactor, RentalPropulsionType, RentalReturnConstraint } from '@motis-project/motis-client'; import { FlagTriangleLeft, Fuel, PlugZap, RefreshCcw, Zap, type Icon as LucideIcon } from '@lucide/svelte'; import { t } from '$lib/i18n/translation'; export type FormFactorAssets = { svg: string; station: string; vehicle: string; cluster: string; label: string; }; type IconDimensions = { width: number; height: number; }; export type MapLibreImageSource = ImageBitmap | HTMLImageElement; export const DEFAULT_FORM_FACTOR: RentalFormFactor = 'BICYCLE'; export const ICON_TYPES = ['station', 'vehicle', 'cluster'] as const; export type IconType = (typeof ICON_TYPES)[number]; export const ICON_BASE_PATH = 'icons/rental/'; export const formFactorAssets: Record = { BICYCLE: { svg: 'bike', station: 'bike_station', vehicle: 'floating_bike', cluster: 'floating_bike_cluster', label: t.bike }, CARGO_BICYCLE: { svg: 'cargo_bike', station: 'cargo_bike_station', vehicle: 'floating_cargo_bike', cluster: 'floating_cargo_bike_cluster', label: t.cargoBike }, CAR: { svg: 'car', station: 'car_station', vehicle: 'floating_car', cluster: 'floating_car_cluster', label: t.car }, MOPED: { svg: 'moped', station: 'moped_station', vehicle: 'floating_moped', cluster: 'floating_moped_cluster', label: t.moped }, SCOOTER_SEATED: { svg: 'seated_scooter', station: 'seated_scooter_station', vehicle: 'floating_seated_scooter', cluster: 'floating_seated_scooter_cluster', label: t.scooterSeated }, SCOOTER_STANDING: { svg: 'scooter', station: 'scooter_station', vehicle: 'floating_scooter', cluster: 'floating_scooter_cluster', label: t.scooterStanding }, OTHER: { svg: 'other', station: 'other_station', vehicle: 'floating_other', cluster: 'floating_other_cluster', label: t.unknownVehicleType } }; export const propulsionTypes: Record< RentalPropulsionType, { component: typeof LucideIcon; title: string } | null > = { ELECTRIC: { component: Zap, title: t.electric }, ELECTRIC_ASSIST: { component: Zap, title: t.electricAssist }, HYBRID: { component: PlugZap, title: t.hybrid }, PLUG_IN_HYBRID: { component: PlugZap, title: t.plugInHybrid }, COMBUSTION: { component: Fuel, title: t.combustion }, COMBUSTION_DIESEL: { component: Fuel, title: t.combustionDiesel }, HYDROGEN_FUEL_CELL: { component: Fuel, title: t.hydrogenFuelCell }, HUMAN: null }; export const returnConstraints: Record< RentalReturnConstraint, { component: typeof LucideIcon; title: string } | null > = { ANY_STATION: { component: FlagTriangleLeft, title: t.returnOnlyAtStations }, ROUNDTRIP_STATION: { component: RefreshCcw, title: t.roundtripStationReturnConstraint }, NONE: null }; export const getIconBaseName = (formFactor: RentalFormFactor, type: IconType) => formFactorAssets[formFactor][type]; export const getIconUrl = (formFactor: RentalFormFactor, type: IconType) => `${ICON_BASE_PATH}${getIconBaseName(formFactor, type)}.svg`; const iconTypeDimensions: Record = { station: { width: 43, height: 44 }, vehicle: { width: 27, height: 27 }, cluster: { width: 35, height: 36 } }; export const getIconDimensions = (type: IconType): IconDimensions => iconTypeDimensions[type]; export async function colorizeIcon( svgUrl: string, color: string, dimensions: IconDimensions ): Promise { if (!browser) { throw new Error('colorizeIcon is not supported in this environment'); } const response = await fetch(svgUrl); if (!response.ok) { throw new Error(`Failed to load icon: ${response.status} ${response.statusText}`); } const svgContent = await response.text(); const parser = new DOMParser(); const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml'); if (svgDoc.getElementsByTagName('parsererror').length > 0) { throw new Error('Invalid SVG content'); } const rootElement = svgDoc.documentElement; if (!(rootElement instanceof SVGSVGElement)) { throw new Error('Provided file is not an SVG'); } const svgRoot = rootElement; if (!svgRoot.getAttribute('xmlns')) { svgRoot.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); } const existingStyle = svgRoot.getAttribute('style'); const colorStyle = `color: ${color}`; const mergedStyle = existingStyle ? `${existingStyle};${colorStyle}` : colorStyle; svgRoot.setAttribute('style', mergedStyle); svgRoot.setAttribute('color', color); svgRoot.setAttribute('width', `${dimensions.width}`); svgRoot.setAttribute('height', `${dimensions.height}`); const serializer = new XMLSerializer(); const serialized = serializer.serializeToString(svgDoc); const blob = new Blob([serialized], { type: 'image/svg+xml;charset=utf-8' }); if (typeof createImageBitmap === 'function') { try { const bitmap = await createImageBitmap(blob); return bitmap; } catch (_error) {} // eslint-disable-line } return await new Promise((resolve, reject) => { const blob_url = URL.createObjectURL(blob); const image = new Image(); image.crossOrigin = 'anonymous'; image.decoding = 'async'; image.onload = () => { URL.revokeObjectURL(blob_url); if (dimensions) { image.width = dimensions.width; image.height = dimensions.height; } resolve(image); }; image.onerror = () => { URL.revokeObjectURL(blob_url); reject(new Error('Failed to load generated image')); }; image.src = blob_url; }); } ================================================ FILE: ui/src/lib/map/rentals/style.ts ================================================ import type { ExpressionSpecification } from 'maplibre-gl'; export const createZoomScaledSize = (baseSize: number): ExpressionSpecification => [ 'interpolate', ['linear'], ['zoom'], 14, baseSize * 0.6, 18, baseSize ]; export const zoomScaledIconSize = createZoomScaledSize(1); export const zoomScaledTextSizeMedium = createZoomScaledSize(12); export const zoomScaledTextSizeSmall = createZoomScaledSize(10); export const createZoomScaledTextOffset = ( baseOffset: [number, number] ): ExpressionSpecification => [ 'interpolate', ['linear'], ['zoom'], 14, ['literal', [baseOffset[0] * 0.8, baseOffset[1] * 0.9]], 18, ['literal', baseOffset] ]; export const zoomScaledTextOffset = createZoomScaledTextOffset([0.8, -1.25]); export const DEFAULT_COLOR = '#2563eb'; export const DEFAULT_CONTRAST_COLOR = '#ffffff'; ================================================ FILE: ui/src/lib/map/rentals/zone-fill-layer.ts ================================================ import earcut from 'earcut'; import { flatten } from 'earcut'; import maplibregl from 'maplibre-gl'; import { type CustomRenderMethodInput, type Map as MapLibreMap, type PointLike } from 'maplibre-gl'; import type { Position } from 'geojson'; import type { RentalZoneFeature, RentalZoneFeatureProperties } from './zone-types'; type GLContext = WebGLRenderingContext | WebGL2RenderingContext; const DEFAULT_OPACITY = 0.4; const STRIPE_WIDTH_PX = 6.0; const STRIPE_OPACITY_VARIATION = 0.1; const POSITION_COMPONENTS = 2; const POSITION_STRIDE_BYTES = POSITION_COMPONENTS * Float32Array.BYTES_PER_ELEMENT; const COLOR_COMPONENTS = 4; const COLOR_STRIDE_BYTES = COLOR_COMPONENTS * Float32Array.BYTES_PER_ELEMENT; const FILL_VERTEX_SHADER_SOURCE = `#version 300 es precision highp float; in vec2 a_pos; in vec4 a_color; out vec4 v_color; uniform vec4 u_zone_base; uniform vec4 u_zone_scale_x; uniform vec4 u_zone_scale_y; void main() { v_color = a_color; gl_Position = u_zone_base + a_pos.x * u_zone_scale_x + a_pos.y * u_zone_scale_y; } `; const FILL_FRAGMENT_SHADER_SOURCE = `#version 300 es precision mediump float; in vec4 v_color; out vec4 fragColor; void main() { fragColor = v_color; } `; type ZoneGeometry = { vertices: number[]; // mercator [x0, y0, x1, y1, ...] minX: number; minY: number; maxX: number; maxY: number; }; type FillProgramState = { program: WebGLProgram; positionLocation: number; colorLocation: number; baseLocation: WebGLUniformLocation | null; scaleXLocation: WebGLUniformLocation | null; scaleYLocation: WebGLUniformLocation | null; }; type ZoneFillLayerOptions = { id: string; opacity?: number; }; const QUAD_VERTICES = new Float32Array([-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1]); const SCREEN_VERTEX_SHADER_SOURCE = ` attribute vec2 a_pos; attribute vec2 a_tex_coord; varying vec2 v_tex_coord; void main() { v_tex_coord = a_tex_coord; gl_Position = vec4(a_pos, 0.0, 1.0); } `; const SCREEN_FRAGMENT_SHADER_SOURCE = ` precision highp float; uniform sampler2D u_texture; uniform float u_opacity_primary; uniform float u_opacity_secondary; uniform float u_stripe_width; varying vec2 v_tex_coord; void main() { vec4 color = texture2D(u_texture, v_tex_coord); if (color.a == 0.0) { discard; } float diagonal = gl_FragCoord.x + gl_FragCoord.y; float stripeIndex = mod(floor(diagonal / u_stripe_width), 2.0); float opacity = mix(u_opacity_primary, u_opacity_secondary, stripeIndex); gl_FragColor = vec4(color.rgb, color.a * opacity); } `; const ZONE_COLOR_ALLOWED = new Float32Array([0.13333333, 0.77254902, 0.36862745, 1]); // #22c55e (green) const ZONE_COLOR_FORBIDDEN = new Float32Array([0.9372549, 0.26666667, 0.26666667, 1]); // #ef4444 (red) const ZONE_COLOR_RESTRICTED = new Float32Array([1, 0.84313725, 0, 1]); // #ffd700 (yellow) const ZONE_COLOR_STATION = new Float32Array([0.25882354, 0.52156866, 0.95686275, 1]); // #4287f5 (blue) export const getZoneColor = (properties: RentalZoneFeatureProperties) => { if (properties.stationArea) { return ZONE_COLOR_STATION; } if (properties.rideEndAllowed) { return ZONE_COLOR_ALLOWED; } if (!properties.rideThroughAllowed) { return ZONE_COLOR_FORBIDDEN; } return ZONE_COLOR_RESTRICTED; }; const encodePickingColor = (i: number): Float32Array => { return new Float32Array([ ((i >> 16) & 0xff) / 0xff, ((i >> 8) & 0xff) / 0xff, (i & 0xff) / 0xff, 1 ]); }; const decodePickingColor = (px: Uint8Array): number => { return (px[0] << 16) | (px[1] << 8) | px[2]; }; type ZoneClipFrame = { base: Float32Array; scaleX: Float32Array; scaleY: Float32Array; }; const toClipCoordinates = ( map: MapLibreMap, width: number, height: number, pixelRatioX: number, pixelRatioY: number, lngLat: maplibregl.LngLat ): Float32Array => { const point = map.project(lngLat); const px = point.x * pixelRatioX; const py = point.y * pixelRatioY; const clipX = (px / width) * 2 - 1; const clipY = 1 - (py / height) * 2; return new Float32Array([clipX, clipY, 0, 1]); }; const computeZoneClipFrame = ( map: MapLibreMap, width: number, height: number, pixelRatioX: number, pixelRatioY: number, origin: Float32Array, extent: Float32Array ): ZoneClipFrame => { const originLngLat = new maplibregl.MercatorCoordinate(origin[0], origin[1]).toLngLat(); const maxLngLat = new maplibregl.MercatorCoordinate( origin[0] + extent[0], origin[1] + extent[1] ).toLngLat(); const base = toClipCoordinates(map, width, height, pixelRatioX, pixelRatioY, originLngLat); const maxClip = toClipCoordinates(map, width, height, pixelRatioX, pixelRatioY, maxLngLat); const scaleX = new Float32Array([maxClip[0] - base[0], 0, 0, 0]); const scaleY = new Float32Array([0, maxClip[1] - base[1], 0, 0]); return { base, scaleX, scaleY }; }; const toPoint = (p: PointLike): maplibregl.Point => { return Array.isArray(p) ? new maplibregl.Point(p[0], p[1]) : p; }; const WEB_MERCATOR_MAX_LATITUDE = 85.051129; const clampLngLatToWebMercator = (lng: number, lat: number): [number, number] => { return [ Math.min(180, Math.max(-180, lng)), Math.min(WEB_MERCATOR_MAX_LATITUDE, Math.max(-WEB_MERCATOR_MAX_LATITUDE, lat)) ]; }; export class ZoneFillLayer implements maplibregl.CustomLayerInterface { id: string; type: 'custom' = 'custom' as const; renderingMode: '2d' = '2d' as const; private opacity: number; private gl: GLContext | null = null; private map: MapLibreMap | null = null; private screenProgram: WebGLProgram | null = null; private framebuffer: WebGLFramebuffer | null = null; private texture: WebGLTexture | null = null; private pickingFramebuffer: WebGLFramebuffer | null = null; private pickingTexture: WebGLTexture | null = null; private quadBuffer: WebGLBuffer | null = null; private features: RentalZoneFeature[] = []; private geometryDirty = true; private width = 0; private height = 0; private pickingWidth = 0; private pickingHeight = 0; private fillProgram: FillProgramState | null = null; private pickingLookup = new Map(); private pickingPixel = new Uint8Array(4); private positionBuffer: WebGLBuffer | null = null; private colorBuffer: WebGLBuffer | null = null; private pickingColorBuffer: WebGLBuffer | null = null; private vertexCount = 0; private globalOrigin = new Float32Array([0, 0]); private globalExtent = new Float32Array([1, 1]); private screenPositionLocation = -1; private screenTexCoordLocation = -1; private screenTextureLocation: WebGLUniformLocation | null = null; private screenOpacityPrimaryLocation: WebGLUniformLocation | null = null; private screenOpacitySecondaryLocation: WebGLUniformLocation | null = null; private screenStripeWidthLocation: WebGLUniformLocation | null = null; constructor(options: ZoneFillLayerOptions) { this.id = options.id; this.opacity = options.opacity ?? DEFAULT_OPACITY; } setOpacity(opacity: number) { if (this.opacity === opacity) { return; } this.opacity = opacity; this.map?.triggerRepaint(); } setFeatures(features: RentalZoneFeature[]) { this.features = features; this.geometryDirty = true; this.updateGeometry(); this.map?.triggerRepaint(); } onAdd(map: MapLibreMap, gl: GLContext) { this.map = map; this.gl = gl; this.initialize(gl); this.updateGeometry(); } onRemove(_map: MapLibreMap, gl: GLContext) { this.cleanup(gl); } cleanup(gl?: GLContext) { if (!gl && this.gl) { gl = this.gl; } if (!gl) { return; } this.deleteGeometryBuffers(gl); if (this.framebuffer) { gl.deleteFramebuffer(this.framebuffer); this.framebuffer = null; } if (this.texture) { gl.deleteTexture(this.texture); this.texture = null; } if (this.pickingFramebuffer) { gl.deleteFramebuffer(this.pickingFramebuffer); this.pickingFramebuffer = null; } if (this.pickingTexture) { gl.deleteTexture(this.pickingTexture); this.pickingTexture = null; } if (this.quadBuffer) { gl.deleteBuffer(this.quadBuffer); this.quadBuffer = null; } if (this.fillProgram) { gl.deleteProgram(this.fillProgram.program); this.fillProgram = null; } this.pickingLookup.clear(); if (this.screenProgram) { gl.deleteProgram(this.screenProgram); this.screenProgram = null; } this.geometryDirty = true; this.width = 0; this.height = 0; this.pickingWidth = 0; this.pickingHeight = 0; this.gl = null; this.map = null; } prerender(gl: GLContext, _options: CustomRenderMethodInput) { if ( !this.map || this.vertexCount === 0 || !this.positionBuffer || !this.colorBuffer || !this.pickingColorBuffer ) { return; } const map = this.map; const width = gl.drawingBufferWidth; const height = gl.drawingBufferHeight; if (width === 0 || height === 0) { return; } const fillProgram = this.ensureFillProgram(gl); if (!fillProgram || fillProgram.positionLocation < 0 || fillProgram.colorLocation < 0) { return; } const canvas = map.getCanvas(); const rect = canvas.getBoundingClientRect(); const cssWidth = rect.width; const cssHeight = rect.height; if (cssWidth === 0 || cssHeight === 0) { return; } const pixelRatioX = width / cssWidth; const pixelRatioY = height / cssHeight; const clipFrame = computeZoneClipFrame( map, width, height, pixelRatioX, pixelRatioY, this.globalOrigin, this.globalExtent ); this.ensureFramebuffer(gl, width, height); this.ensurePickingFramebuffer(gl, width, height); if (!this.framebuffer || !this.texture || !this.pickingFramebuffer || !this.pickingTexture) { return; } const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null; const previousViewport = gl.getParameter(gl.VIEWPORT) as Int32Array; const blendEnabled = gl.isEnabled(gl.BLEND); const depthTestEnabled = gl.isEnabled(gl.DEPTH_TEST); const stencilTestEnabled = gl.isEnabled(gl.STENCIL_TEST); const cullFaceEnabled = gl.isEnabled(gl.CULL_FACE); gl.disable(gl.BLEND); gl.disable(gl.DEPTH_TEST); gl.disable(gl.STENCIL_TEST); gl.disable(gl.CULL_FACE); const renderToFramebuffer = (fb: WebGLFramebuffer, colorBuffer: WebGLBuffer) => { gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.viewport(0, 0, width, height); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(fillProgram.program); gl.uniform4fv(fillProgram.baseLocation, clipFrame.base); gl.uniform4fv(fillProgram.scaleXLocation, clipFrame.scaleX); gl.uniform4fv(fillProgram.scaleYLocation, clipFrame.scaleY); gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer); gl.enableVertexAttribArray(fillProgram.positionLocation); gl.vertexAttribPointer( fillProgram.positionLocation, POSITION_COMPONENTS, gl.FLOAT, false, POSITION_STRIDE_BYTES, 0 ); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.enableVertexAttribArray(fillProgram.colorLocation); gl.vertexAttribPointer( fillProgram.colorLocation, COLOR_COMPONENTS, gl.FLOAT, false, COLOR_STRIDE_BYTES, 0 ); gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount); gl.disableVertexAttribArray(fillProgram.positionLocation); gl.disableVertexAttribArray(fillProgram.colorLocation); gl.bindBuffer(gl.ARRAY_BUFFER, null); }; renderToFramebuffer(this.framebuffer, this.colorBuffer); renderToFramebuffer(this.pickingFramebuffer, this.pickingColorBuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer); gl.viewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]); if (blendEnabled) { gl.enable(gl.BLEND); } if (depthTestEnabled) { gl.enable(gl.DEPTH_TEST); } if (stencilTestEnabled) { gl.enable(gl.STENCIL_TEST); } if (cullFaceEnabled) { gl.enable(gl.CULL_FACE); } } render(gl: GLContext, _options: CustomRenderMethodInput) { if (!this.screenProgram || !this.texture || !this.quadBuffer || this.vertexCount === 0) { return; } gl.useProgram(this.screenProgram); const prevBlendSrcRGB = gl.getParameter(gl.BLEND_SRC_RGB) as number; const prevBlendDstRGB = gl.getParameter(gl.BLEND_DST_RGB) as number; const prevBlendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA) as number; const prevBlendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA) as number; const prevBlendEquationRGB = gl.getParameter(gl.BLEND_EQUATION_RGB) as number; const prevBlendEquationAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA) as number; gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enableVertexAttribArray(this.screenPositionLocation); gl.enableVertexAttribArray(this.screenTexCoordLocation); gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer); gl.vertexAttribPointer(this.screenPositionLocation, 2, gl.FLOAT, false, 16, 0); gl.vertexAttribPointer(this.screenTexCoordLocation, 2, gl.FLOAT, false, 16, 8); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.uniform1i(this.screenTextureLocation, 0); const minOpacity = Math.max(this.opacity - STRIPE_OPACITY_VARIATION, 0.0); const maxOpacity = Math.min(this.opacity + STRIPE_OPACITY_VARIATION, 1.0); gl.uniform1f(this.screenOpacityPrimaryLocation, minOpacity); gl.uniform1f(this.screenOpacitySecondaryLocation, maxOpacity); gl.uniform1f(this.screenStripeWidthLocation, STRIPE_WIDTH_PX); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.disableVertexAttribArray(this.screenPositionLocation); gl.disableVertexAttribArray(this.screenTexCoordLocation); gl.blendFuncSeparate(prevBlendSrcRGB, prevBlendDstRGB, prevBlendSrcAlpha, prevBlendDstAlpha); gl.blendEquationSeparate(prevBlendEquationRGB, prevBlendEquationAlpha); } private initialize(gl: GLContext) { this.screenProgram = this.createProgram( gl, SCREEN_VERTEX_SHADER_SOURCE, SCREEN_FRAGMENT_SHADER_SOURCE ); if (!this.screenProgram) { throw new Error('Failed to initialize zone fill shaders'); } this.screenPositionLocation = gl.getAttribLocation(this.screenProgram, 'a_pos'); this.screenTexCoordLocation = gl.getAttribLocation(this.screenProgram, 'a_tex_coord'); this.screenTextureLocation = gl.getUniformLocation(this.screenProgram, 'u_texture'); this.screenOpacityPrimaryLocation = gl.getUniformLocation( this.screenProgram, 'u_opacity_primary' ); this.screenOpacitySecondaryLocation = gl.getUniformLocation( this.screenProgram, 'u_opacity_secondary' ); this.screenStripeWidthLocation = gl.getUniformLocation(this.screenProgram, 'u_stripe_width'); this.quadBuffer = gl.createBuffer(); if (!this.quadBuffer) { throw new Error('Failed to allocate quad buffer'); } gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer); gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); } private ensureFramebuffer(gl: GLContext, width: number, height: number) { if (this.framebuffer && this.texture && this.width === width && this.height === height) { return; } this.width = width; this.height = height; if (!this.framebuffer) { this.framebuffer = gl.createFramebuffer(); } if (!this.texture) { this.texture = gl.createTexture(); } if (!this.framebuffer || !this.texture) { return; } gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0); const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error('[ZoneFillLayer] Incomplete framebuffer:', status); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); } private ensurePickingFramebuffer(gl: GLContext, width: number, height: number) { if ( this.pickingFramebuffer && this.pickingTexture && this.pickingWidth === width && this.pickingHeight === height ) { return; } if (!this.pickingFramebuffer) { this.pickingFramebuffer = gl.createFramebuffer(); } if (!this.pickingTexture) { this.pickingTexture = gl.createTexture(); } if (!this.pickingFramebuffer || !this.pickingTexture) { return; } gl.bindTexture(gl.TEXTURE_2D, this.pickingTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.bindFramebuffer(gl.FRAMEBUFFER, this.pickingFramebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.pickingTexture, 0 ); const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (status !== gl.FRAMEBUFFER_COMPLETE) { console.error('[ZoneFillLayer] Incomplete picking framebuffer:', status); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); this.pickingWidth = width; this.pickingHeight = height; } private ensureFillProgram(gl: GLContext): FillProgramState | null { if (this.fillProgram) { return this.fillProgram; } const program = this.createProgram(gl, FILL_VERTEX_SHADER_SOURCE, FILL_FRAGMENT_SHADER_SOURCE); if (!program) { return null; } const state: FillProgramState = { program, positionLocation: gl.getAttribLocation(program, 'a_pos'), colorLocation: gl.getAttribLocation(program, 'a_color'), baseLocation: gl.getUniformLocation(program, 'u_zone_base'), scaleXLocation: gl.getUniformLocation(program, 'u_zone_scale_x'), scaleYLocation: gl.getUniformLocation(program, 'u_zone_scale_y') }; this.fillProgram = state; return state; } private createProgram( gl: GLContext, vertexSrc: string, fragmentSrc: string ): WebGLProgram | null { const vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, vertexSrc); const fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc); if (!vertexShader || !fragmentShader) { return null; } const program = gl.createProgram(); if (!program) { return null; } gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('Failed to link shader program', gl.getProgramInfoLog(program)); gl.deleteProgram(program); return null; } gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); return program; } private compileShader(gl: GLContext, type: number, source: string): WebGLShader | null { const shader = gl.createShader(type); if (!shader) { return null; } gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Failed to compile shader', gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } private updateGeometry() { const gl = this.gl; if (!gl || !this.geometryDirty) { return; } this.deleteGeometryBuffers(gl); this.pickingLookup.clear(); const features = [...this.features].sort((a, b) => a.properties.z - b.properties.z); const zoneGeometries: ZoneGeometry[] = []; const colorValues: number[] = []; const pickingValues: number[] = []; let totalVertices = 0; let globalMinX = Number.POSITIVE_INFINITY; let globalMinY = Number.POSITIVE_INFINITY; let globalMaxX = Number.NEGATIVE_INFINITY; let globalMaxY = Number.NEGATIVE_INFINITY; let zoneIdx = 0; for (const feature of features) { const geometry = this.buildZoneGeometry(feature); if (!geometry) { continue; } const vertexCount = geometry.vertices.length / POSITION_COMPONENTS; if (vertexCount === 0) { continue; } zoneGeometries.push(geometry); totalVertices += vertexCount; globalMinX = Math.min(globalMinX, geometry.minX); globalMinY = Math.min(globalMinY, geometry.minY); globalMaxX = Math.max(globalMaxX, geometry.maxX); globalMaxY = Math.max(globalMaxY, geometry.maxY); const pickingIdx = zoneIdx + 1; zoneIdx += 1; const color = getZoneColor(feature.properties); const pickingColor = encodePickingColor(pickingIdx); this.pickingLookup.set(pickingIdx, feature); for (let i = 0; i < vertexCount; ++i) { colorValues.push(color[0], color[1], color[2], color[3]); pickingValues.push(pickingColor[0], pickingColor[1], pickingColor[2], pickingColor[3]); } } const extentX = globalMaxX - globalMinX; const extentY = globalMaxY - globalMinY; if ( totalVertices === 0 || globalMinX === Number.POSITIVE_INFINITY || globalMinY === Number.POSITIVE_INFINITY || globalMaxX === Number.NEGATIVE_INFINITY || globalMaxY === Number.NEGATIVE_INFINITY || extentX === 0 || extentY === 0 ) { this.vertexCount = 0; this.geometryDirty = false; return; } this.globalOrigin = new Float32Array([globalMinX, globalMinY]); this.globalExtent = new Float32Array([extentX, extentY]); const positions = new Float32Array(totalVertices * POSITION_COMPONENTS); let posOffset = 0; for (const geometry of zoneGeometries) { for (let i = 0; i < geometry.vertices.length; i += POSITION_COMPONENTS) { const x = geometry.vertices[i]; const y = geometry.vertices[i + 1]; positions[posOffset++] = (x - globalMinX) / extentX; positions[posOffset++] = (y - globalMinY) / extentY; } } const colorArray = new Float32Array(colorValues); const pickingArray = new Float32Array(pickingValues); this.positionBuffer = gl.createBuffer(); this.colorBuffer = gl.createBuffer(); this.pickingColorBuffer = gl.createBuffer(); if (!this.positionBuffer || !this.colorBuffer || !this.pickingColorBuffer) { console.error('[ZoneFillLayer] Failed to allocate geometry buffers'); this.deleteGeometryBuffers(gl); this.geometryDirty = false; return; } gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this.pickingColorBuffer); gl.bufferData(gl.ARRAY_BUFFER, pickingArray, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); this.vertexCount = totalVertices; this.geometryDirty = false; } private deleteGeometryBuffers(gl: GLContext) { if (this.positionBuffer) { gl.deleteBuffer(this.positionBuffer); this.positionBuffer = null; } if (this.colorBuffer) { gl.deleteBuffer(this.colorBuffer); this.colorBuffer = null; } if (this.pickingColorBuffer) { gl.deleteBuffer(this.pickingColorBuffer); this.pickingColorBuffer = null; } this.vertexCount = 0; this.globalOrigin = new Float32Array([0, 0]); this.globalExtent = new Float32Array([1, 1]); } pickFeatureAt(pointLike: PointLike): RentalZoneFeature | null { if (!this.map || !this.gl || !this.pickingFramebuffer || !this.pickingTexture) { return null; } const pt = toPoint(pointLike); const canvas = this.map.getCanvas(); const rect = canvas.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) { return null; } const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const pixelX = Math.floor(pt.x * scaleX); const pixelY = Math.floor(canvas.height - pt.y * scaleY - 1); const previousFramebuffer = this.gl.getParameter( this.gl.FRAMEBUFFER_BINDING ) as WebGLFramebuffer | null; this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.pickingFramebuffer); this.gl.readPixels( pixelX, pixelY, 1, 1, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.pickingPixel ); this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer); const index = decodePickingColor(this.pickingPixel); if (index === 0) { return null; } return this.pickingLookup.get(index) ?? null; } private buildZoneGeometry(feature: RentalZoneFeature): ZoneGeometry | null { const vertices: number[] = []; let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; const appendPoly = (coords: Position[][]) => { if (!coords.length) { return; } const mercatorCoords = coords.map((ring) => ring.map(([lng, lat]) => { const clamped = clampLngLatToWebMercator(lng, lat); const merc = maplibregl.MercatorCoordinate.fromLngLat(clamped); minX = Math.min(minX, merc.x); minY = Math.min(minY, merc.y); maxX = Math.max(maxX, merc.x); maxY = Math.max(maxY, merc.y); return [merc.x, merc.y]; }) ); const data = flatten(mercatorCoords); const indices = earcut(data.vertices, data.holes, data.dimensions); const stride = data.dimensions; for (const index of indices) { const base = index * stride; vertices.push(data.vertices[base], data.vertices[base + 1]); } }; for (const polygon of feature.geometry.coordinates) { appendPoly(polygon); } if (vertices.length === 0) { return null; } return { vertices, minX, minY, maxX, maxY }; } } ================================================ FILE: ui/src/lib/map/rentals/zone-types.ts ================================================ import type { Feature, FeatureCollection, MultiPolygon } from 'geojson'; export type RentalZoneFeatureProperties = { zoneIndex?: number; stationIndex?: number; providerId: string; z: number; rideEndAllowed: boolean; rideThroughAllowed: boolean; stationArea: boolean; }; export type RentalZoneFeature = Feature; export type RentalZoneFeatureCollection = FeatureCollection< MultiPolygon, RentalZoneFeatureProperties >; ================================================ FILE: ui/src/lib/map/routes/Routes.svelte ================================================ {#snippet routesPopup( event: maplibregl.MapMouseEvent, closePopup: () => void, features: maplibregl.MapGeoJSONFeature[] | undefined )} {@const popupPoint = getPopupPoint(event.lngLat, event.point)} {@const popupFeatures = features?.length ? features : getRouteFeaturesAtPoint(event.lngLat, event.point)} {@const displayedRouteIdxs = getDisplayedRouteIdxs(features, popupFeatures)} {@const routesAtPoint = getRoutesFromRouteIdxs(displayedRouteIdxs, popupFeatures, popupPoint)} {/snippet} {#if focusedRoute} {@const focusedDisplay = getRouteDisplayProps(focusedRoute)}
{formatRouteNames(focusedRoute)}
{#if shapesDebugEnabled} {/if}
Index
{focusedRoute.routeIdx}
Mode
{getRouteModeName(focusedRoute.mode)}
IDs
{#if focusedRoute.transitRoutes.length} {#each focusedRoute.transitRoutes as transitRoute, i (transitRoute.id + i)}
{transitRoute.id}
{/each} {:else}
{/if}
Names
{#if focusedRoute.transitRoutes.length} {#each focusedRoute.transitRoutes as transitRoute, i (transitRoute.id + i)}
{transitRoute.shortName || transitRoute.longName}
{/each} {:else}
{/if}
Stops
{focusedRoute.numStops}
Source
{focusedRoute.pathSource}
{/if} ================================================ FILE: ui/src/lib/map/shield.ts ================================================ import { browser } from '$app/environment'; import type { StyleImageMetadata } from 'maplibre-gl'; class ShieldOptions { fill!: string; stroke!: string; } export function createShield(opt: ShieldOptions): [ImageData, Partial] { if (!browser) { throw 'not supported'; } const d = 32; const cv = document.createElement('canvas'); cv.width = d; cv.height = d; const ctx = cv.getContext('2d')!; // coord of the line (front = near zero, back = opposite) const l_front = 1; const l_back = d - 1; // coord start of the arc const lr_front = l_front + 2; const lr_back = l_back - 2; // control point of the arc const lp_front = l_front + 1; const lp_back = l_back - 1; const p = new Path2D(); p.moveTo(lr_front, l_front); // top line p.lineTo(lr_back, l_front); // top right corner p.bezierCurveTo(lp_back, lp_front, lp_back, lp_front, l_back, lr_front); // right line p.lineTo(l_back, lr_back); // bottom right corner p.bezierCurveTo(lp_back, lp_back, lp_back, lp_back, lr_back, l_back); // bottom line p.lineTo(lr_front, l_back); // bottom left corner p.bezierCurveTo(lp_front, lp_back, lp_front, lp_back, l_front, lr_back); // left line p.lineTo(l_front, lr_front); // top left corner p.bezierCurveTo(lp_front, lp_front, lp_front, lp_front, lr_front, l_front); p.closePath(); ctx.fillStyle = opt.fill; ctx.fill(p); ctx.strokeStyle = opt.stroke; ctx.stroke(p); return [ ctx.getImageData(0, 0, d, d), { content: [lr_front, lr_front, lr_back, lr_back], stretchX: [[lr_front, lr_back]], stretchY: [[lr_front, lr_back]] } ]; } ================================================ FILE: ui/src/lib/map/stops/StopsGeoJSON.svelte ================================================ { const s = e.features?.[0]; if (!s?.properties?.stopId) { return; } console.log('Clicked Stop:', s.properties.name); onClickStop(s.properties.name, s.properties.stopId, new Date(s.properties.time)); }} onmousemove={(_, map) => (map.getCanvas().style.cursor = 'pointer')} onmouseleave={(_, map) => (map.getCanvas().style.cursor = '')} /> ================================================ FILE: ui/src/lib/map/style.ts ================================================ import type { HillshadeLayerSpecification, RasterDEMSourceSpecification, StyleSpecification } from 'maplibre-gl'; const colors = { light: { background: '#f8f4f0', water: '#99ddff', rail: '#a8a8a8', pedestrian: '#e8e7eb', ferryRoute: 'rgba(102, 102, 255, 0.5)', sport: '#d0f4be', sportOutline: '#b3e998', building: '#ded7d3', buildingOutline: '#cfc8c4', landuseComplex: '#f0e6d1', landuseCommercial: 'hsla(0, 60%, 87%, 0.23)', landuseIndustrial: '#e0e2eb', landuseResidential: '#ece7e4', landuseRetail: 'hsla(0, 60%, 87%, 0.23)', landuseConstruction: '#aaa69d', landusePark: '#b8ebad', landuseNatureLight: '#ddecd7', landuseNatureHeavy: '#a3e39c', landuseCemetery: '#e0e4dd', landuseBeach: '#fffcd3', indoorCorridor: '#fdfcfa', indoor: '#d4edff', indoorOutline: '#808080', indoorText: '#333333', publicTransport: 'rgba(218,140,140,0.3)', footway: '#fff', steps: '#ff4524', elevatorOutline: '#808080', elevator: '#bcf1ba', roadBackResidential: '#ffffff', roadBackNonResidential: '#ffffff', motorway: '#ffb366', motorwayLink: '#f7e06e', primarySecondary: '#fffbf8', linkTertiary: '#ffffff', residential: '#ffffff', road: '#ffffff', townText: '#333333', townTextHalo: 'white', text: '#333333', textHalo: 'white', citiesText: '#111111', citiesTextHalo: 'white', shield: 'shield' }, dark: { background: '#292929', water: '#1f2830', rail: '#808080', pedestrian: '#292929', ferryRoute: 'rgba(58, 77, 139, 0.5)', sport: '#272525', sportOutline: '#272525', building: '#1F1F1F', buildingOutline: '#1A1A1A', landuseComplex: '#292929', landuseCommercial: '#292929', landuseIndustrial: '#353538', landuseResidential: '#292929', landuseRetail: '#292929', landuseConstruction: 'red', landusePark: '#18221f', landuseNatureLight: '#1e2322', landuseNatureHeavy: '#1a2020', landuseCemetery: '#202423', landuseBeach: '#4c4b3e', indoorCorridor: '#494949', indoor: '#1a1a1a', indoorOutline: '#0d0d0d', indoorText: '#eeeeee', publicTransport: 'rgba(89, 45, 45, 0.405)', footway: '#3D3D3D', steps: '#70504b', elevatorOutline: '#808080', elevator: '#3b423b', roadBackResidential: '#414141', roadBackNonResidential: '#414141', motorway: '#414141', motorwayLink: '#414141', primarySecondary: '#414141', linkTertiary: '#414141', residential: '#414141', road: '#414141', text: '#9a9a9a', textHalo: '#151515', townText: '#bebebe', townTextHalo: '#1A1A1A', citiesText: '#bebebe', citiesTextHalo: '#1A1A1A', shield: 'shield-dark' } }; function getUrlBase(url: string): string { const { origin, pathname } = new URL(url); return origin + pathname.slice(0, pathname.lastIndexOf('/') + 1); } // this doesn't escape {}-parameters function getAbsoluteUrl(base: string, relative: string): string { return getUrlBase(base) + relative; } export const getStyle = ( theme: 'light' | 'dark', level: number, staticBaseUrl: string, apiBaseUrl: string, withHillshades: boolean ): StyleSpecification => { const c = colors[theme]; const hillshadeSources: StyleSpecification['sources'] = withHillshades ? { hillshadeSource: { type: 'raster-dem', url: 'https://tiles.mapterhorn.com/tilejson.json' } satisfies RasterDEMSourceSpecification } : {}; const hillshadeLayers: HillshadeLayerSpecification[] = withHillshades ? [ { id: 'hillshade', type: 'hillshade', source: 'hillshadeSource', paint: { 'hillshade-exaggeration': 0.33 } } ] : []; return { version: 8, sources: { osm: { type: 'vector', tiles: [getAbsoluteUrl(apiBaseUrl, 'tiles/{z}/{x}/{y}.mvt')], maxzoom: 20, attribution: '' }, ...hillshadeSources }, glyphs: getAbsoluteUrl(apiBaseUrl, 'tiles/glyphs/{fontstack}/{range}.pbf'), sprite: getAbsoluteUrl(staticBaseUrl, 'sprite_sdf'), layers: [ { id: 'background', type: 'background', paint: { 'background-color': c.background } }, { id: 'coastline', type: 'fill', source: 'osm', 'source-layer': 'coastline', paint: { 'fill-color': c.water } }, { id: 'landuse_park', type: 'fill', source: 'osm', 'source-layer': 'landuse', filter: ['==', ['get', 'landuse'], 'park'], paint: { 'fill-color': c.landusePark } }, { id: 'landuse', type: 'fill', source: 'osm', 'source-layer': 'landuse', filter: ['!in', 'landuse', 'park', 'public_transport'], paint: { 'fill-color': [ 'match', ['get', 'landuse'], 'complex', c.landuseComplex, 'commercial', c.landuseCommercial, 'industrial', c.landuseIndustrial, 'residential', c.landuseResidential, 'retail', c.landuseRetail, 'construction', c.landuseConstruction, 'nature_light', c.landuseNatureLight, 'nature_heavy', c.landuseNatureHeavy, 'cemetery', c.landuseCemetery, 'beach', c.landuseBeach, 'magenta' ] } }, { id: 'water', type: 'fill', source: 'osm', 'source-layer': 'water', paint: { 'fill-color': c.water } }, { id: 'sport', type: 'fill', source: 'osm', 'source-layer': 'sport', paint: { 'fill-color': c.sport, 'fill-outline-color': c.sportOutline } }, { id: 'pedestrian', type: 'fill', source: 'osm', 'source-layer': 'pedestrian', paint: { 'fill-color': c.pedestrian } }, { id: 'waterway', type: 'line', source: 'osm', 'source-layer': 'waterway', paint: { 'line-color': c.water } }, { id: 'building', type: 'fill', source: 'osm', 'source-layer': 'building', paint: { 'fill-color': c.building, 'fill-outline-color': c.buildingOutline, 'fill-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 16, 0.8] } }, { id: 'indoor-corridor', type: 'fill', source: 'osm', 'source-layer': 'indoor', filter: ['all', ['==', 'indoor', 'corridor'], ['==', 'level', level]], paint: { 'fill-color': c.indoorCorridor, 'fill-opacity': 0.8 } }, { id: 'indoor', type: 'fill', source: 'osm', 'source-layer': 'indoor', filter: ['all', ['!in', 'indoor', 'corridor', 'wall', 'elevator'], ['==', 'level', level]], paint: { 'fill-color': c.indoor, 'fill-opacity': 0.8 } }, { id: 'indoor-outline', type: 'line', source: 'osm', 'source-layer': 'indoor', filter: ['all', ['!in', 'indoor', 'corridor', 'wall', 'elevator'], ['==', 'level', level]], minzoom: 18, paint: { 'line-color': c.indoorOutline, 'line-width': 2 } }, { id: 'indoor-names', type: 'symbol', source: 'osm', 'source-layer': 'indoor', minzoom: 18, filter: ['any', ['!has', 'level'], ['==', 'level', level]], layout: { 'symbol-placement': 'point', 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 12 }, paint: { 'text-color': c.indoorText } }, { id: 'landuse-public-transport', type: 'fill', source: 'osm', 'source-layer': 'landuse', filter: [ 'all', ['==', 'landuse', 'public_transport'], ['any', ['!has', 'level'], ['==', 'level', level]] ], paint: { 'fill-color': c.publicTransport } }, { id: 'footway', type: 'line', source: 'osm', 'source-layer': 'road', filter: [ 'all', ['in', 'highway', 'footway', 'track', 'cycleway', 'path', 'unclassified', 'service'], level === 0 ? ['any', ['!has', 'level'], ['==', 'level', level]] : ['==', 'level', level] ], layout: { 'line-cap': 'round' }, minzoom: 14, paint: { 'line-dasharray': [0.75, 1.5], 'line-color': c.footway, 'line-opacity': 0.5, 'line-width': [ 'let', 'base', 0.4, [ 'interpolate', ['linear'], ['zoom'], 5, ['+', ['*', ['var', 'base'], 0.1], 1], 9, ['+', ['*', ['var', 'base'], 0.4], 1], 12, ['+', ['*', ['var', 'base'], 1], 1], 16, ['+', ['*', ['var', 'base'], 4], 1], 20, ['+', ['*', ['var', 'base'], 8], 1] ] ] } }, { id: 'stairs-ground', type: 'line', source: 'osm', 'source-layer': 'road', minzoom: 18, filter: [ 'all', ['==', 'highway', 'steps'], level === 0 ? [ 'any', ['!has', 'from_level'], ['any', ['==', 'from_level', level], ['==', 'to_level', level]] ] : ['any', ['==', 'from_level', level], ['==', 'to_level', level]] ], paint: { 'line-color': '#ddddddff', 'line-width': [ 'interpolate', ['exponential', 2], ['zoom'], 10, ['*', 4, ['^', 2, -6]], 24, ['*', 4, ['^', 2, 8]] ] } }, { id: 'stairs-steps', type: 'line', source: 'osm', 'source-layer': 'road', minzoom: 18, filter: [ 'all', ['==', 'highway', 'steps'], level === 0 ? [ 'any', ['!has', 'from_level'], ['any', ['==', 'from_level', level], ['==', 'to_level', level]] ] : ['any', ['==', 'from_level', level], ['==', 'to_level', level]] ], paint: { 'line-color': '#bfbfbf', 'line-dasharray': ['literal', [0.01, 0.1]], 'line-width': [ 'interpolate', ['exponential', 2], ['zoom'], 10, ['*', 4, ['^', 2, -6]], 24, ['*', 4, ['^', 2, 8]] ] } }, { id: 'stairs-rail', type: 'line', source: 'osm', 'source-layer': 'road', minzoom: 18, filter: [ 'all', ['==', 'highway', 'steps'], level === 0 ? [ 'any', ['!has', 'from_level'], ['any', ['==', 'from_level', level], ['==', 'to_level', level]] ] : ['any', ['==', 'from_level', level], ['==', 'to_level', level]] ], paint: { 'line-color': '#808080', 'line-width': [ 'interpolate', ['exponential', 2], ['zoom'], 10, ['*', 0.25, ['^', 2, -6]], 24, ['*', 0.25, ['^', 2, 8]] ], 'line-gap-width': [ 'interpolate', ['exponential', 2], ['zoom'], 10, ['*', 4, ['^', 2, -6]], 24, ['*', 4, ['^', 2, 8]] ] } }, { id: 'indoor-elevator-outline', type: 'circle', source: 'osm', 'source-layer': 'indoor', minzoom: 18, filter: [ 'all', ['==', 'indoor', 'elevator'], ['<=', 'from_level', level], ['>=', 'to_level', level] ], paint: { 'circle-color': c.elevatorOutline, 'circle-radius': 16 } }, { id: 'indoor-elevator', type: 'circle', source: 'osm', 'source-layer': 'indoor', minzoom: 18, filter: [ 'all', ['==', 'indoor', 'elevator'], ['<=', 'from_level', level], ['>=', 'to_level', level] ], paint: { 'circle-color': c.elevator, 'circle-radius': 14 } }, { id: 'indoor-elevator-icon', type: 'symbol', source: 'osm', 'source-layer': 'indoor', minzoom: 18, filter: [ 'all', ['==', 'indoor', 'elevator'], ['<=', 'from_level', level], ['>=', 'to_level', level] ], layout: { 'icon-image': 'elevator', 'icon-size': 0.9 } }, { id: 'road_back_residential', type: 'line', source: 'osm', 'source-layer': 'road', filter: ['==', 'highway', 'residential'], layout: { 'line-cap': 'round' }, paint: { 'line-color': c.roadBackResidential, 'line-width': ['interpolate', ['linear'], ['zoom'], 5, 0, 9, 0.5, 12, 1, 16, 4, 20, 20], 'line-opacity': ['interpolate', ['linear'], ['zoom'], 12, 0.4, 15, 1] } }, { id: 'road_back_non_residential', type: 'line', source: 'osm', 'source-layer': 'road', filter: [ '!in', 'highway', 'footway', 'track', 'steps', 'cycleway', 'path', 'unclassified', 'residential', 'service' ], layout: { 'line-cap': 'round' }, paint: { 'line-color': c.roadBackNonResidential, 'line-width': [ 'let', 'base', [ 'match', ['get', 'highway'], 'motorway', 4, ['trunk', 'motorway_link'], 3.5, ['primary', 'secondary', 'aeroway', 'trunk_link'], 3, ['primary_link', 'secondary_link', 'tertiary', 'tertiary_link'], 1.75, 0.0 ], [ 'interpolate', ['linear'], ['zoom'], 5, ['+', ['*', ['var', 'base'], 0.1], 1], 9, ['+', ['*', ['var', 'base'], 0.4], 1], 12, ['+', ['*', ['var', 'base'], 1], 1], 16, ['+', ['*', ['var', 'base'], 4], 1], 20, ['+', ['*', ['var', 'base'], 8], 1] ] ] } }, { id: 'road', type: 'line', source: 'osm', 'source-layer': 'road', layout: { 'line-cap': 'round' }, filter: [ 'all', ['has', 'ref'], [ 'any', ['==', ['get', 'highway'], 'motorway'], ['==', ['get', 'highway'], 'trunk'], ['==', ['get', 'highway'], 'secondary'], ['>', ['zoom'], 11] ] ], paint: { 'line-color': [ 'match', ['get', 'highway'], 'motorway', c.motorway, ['trunk', 'motorway_link'], c.motorwayLink, ['primary', 'secondary', 'aeroway', 'trunk_link'], c.primarySecondary, ['primary_link', 'secondary_link', 'tertiary', 'tertiary_link'], c.linkTertiary, 'residential', c.residential, c.road ], 'line-width': [ 'let', 'base', [ 'match', ['get', 'highway'], 'motorway', 3.5, ['trunk', 'motorway_link'], 3, ['primary', 'secondary', 'aeroway', 'trunk_link'], 2.5, ['primary_link', 'secondary_link', 'tertiary', 'tertiary_link'], 1.75, 'residential', 1.5, 0.75 ], [ 'interpolate', ['linear'], ['zoom'], 5, ['*', ['var', 'base'], 0.5], 9, ['*', ['var', 'base'], 1], 12, ['*', ['var', 'base'], 2], 16, ['*', ['var', 'base'], 2.5], 20, ['*', ['var', 'base'], 3] ] ] } }, ...hillshadeLayers, { id: 'ferry_routes', type: 'line', source: 'osm', 'source-layer': 'ferry', paint: { 'line-width': 1.5, 'line-dasharray': [2, 3], 'line-color': c.ferryRoute } }, { id: 'rail_detail', type: 'line', source: 'osm', 'source-layer': 'rail', filter: ['==', 'rail', 'detail'], paint: { 'line-color': c.rail } }, { id: 'rail_secondary', type: 'line', source: 'osm', 'source-layer': 'rail', filter: [ 'all', ['==', 'rail', 'secondary'], ['any', ['!has', 'level'], ['==', 'level', level]] ], paint: { 'line-color': c.rail, 'line-width': 1.15 } }, { id: 'rail_primary', type: 'line', source: 'osm', 'source-layer': 'rail', filter: [ 'all', ['==', 'rail', 'primary'], ['any', ['!has', 'level'], ['==', 'level', level]] ], paint: { 'line-color': c.rail, 'line-width': 1.3 } }, { id: 'aerialway', type: 'line', source: 'osm', 'source-layer': 'aerialway', paint: { 'line-color': c.rail, 'line-dasharray': [10, 2] } }, { id: 'road-ref-shield', type: 'symbol', source: 'osm', 'source-layer': 'road', minzoom: 6, filter: [ 'all', ['has', 'ref'], [ 'any', ['==', ['get', 'highway'], 'motorway'], ['==', ['get', 'highway'], 'trunk'], ['==', ['get', 'highway'], 'secondary'], ['>', ['zoom'], 11] ] ], layout: { 'symbol-placement': 'line', 'text-field': ['get', 'ref'], 'text-font': ['Noto Sans Regular'], 'text-size': ['case', ['==', ['get', 'highway'], 'motorway'], 11, 10], 'text-justify': 'center', 'text-rotation-alignment': 'viewport', 'text-pitch-alignment': 'viewport', 'icon-image': c.shield, 'icon-text-fit': 'both', 'icon-text-fit-padding': [0.5, 4, 0.5, 4], 'icon-rotation-alignment': 'viewport', 'icon-pitch-alignment': 'viewport' }, paint: { 'text-color': c.text } }, { id: 'road-name-text', type: 'symbol', source: 'osm', 'source-layer': 'road', minzoom: 14, layout: { 'symbol-placement': 'line', 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 9 }, paint: { 'text-halo-width': 11, 'text-halo-color': c.textHalo, 'text-color': c.text } }, { id: 'towns', type: 'symbol', source: 'osm', 'source-layer': 'cities', filter: ['!=', ['get', 'place'], 'city'], layout: { // "symbol-sort-key": ["get", "population"], 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Regular'], 'text-size': 12 }, paint: { 'text-halo-width': 1, 'text-halo-color': c.textHalo, 'text-color': c.text } }, { id: 'cities', type: 'symbol', source: 'osm', 'source-layer': 'cities', filter: ['==', ['get', 'place'], 'city'], layout: { 'symbol-sort-key': ['-', ['coalesce', ['get', 'population'], 0]], 'text-field': ['get', 'name'], 'text-font': ['Noto Sans Bold'], 'text-size': ['interpolate', ['linear'], ['zoom'], 6, 12, 9, 16] }, paint: { 'text-halo-width': 2, 'text-halo-color': 'white', 'text-color': 'hsl(0, 0%, 20%)' } // }, { // "id": "tiles_debug_info_bound", // "type": "line", // "source": "osm", // "source-layer": "tiles_debug_info", // "paint": { // "line-color": "magenta", // "line-width": 1 // } // }, { // "id": "tiles_debug_info_name", // "type": "symbol", // "source": "osm", // "source-layer": "tiles_debug_info", // "layout": { // "text-field": ["get", "tile_id"], // "text-font": ["Noto Sans Bold"], // "text-size": 16, // }, // "paint": { // "text-halo-width": 2, // "text-halo-color": "white", // "text-color": "hsl(0, 0%, 20%)" // } } ] }; }; ================================================ FILE: ui/src/lib/modeStyle.ts ================================================ import type { Mode, Rental } from '@motis-project/motis-client'; export type Colorable = { routeColor?: string; routeTextColor?: string; mode: Mode }; export type TripInfo = { tripId?: string; displayName?: string }; export type RentalInfo = { rental?: Rental }; export type LegLike = Colorable & TripInfo & RentalInfo; export const getModeStyle = (l: LegLike): [string, string, string] => { switch (l.mode) { case 'WALK': return ['walk', 'hsl(var(--foreground) / 1)', 'hsl(var(--background) / 1)']; case 'BIKE': return ['bike', 'hsl(var(--foreground) / 1)', 'hsl(var(--background) / 1)']; case 'RENTAL': switch (l.rental?.formFactor) { case 'BICYCLE': return ['bike', '#075985', 'white']; case 'CARGO_BICYCLE': return ['cargo_bike', '#075985', 'white']; case 'CAR': return ['car', '#4c4947', 'white']; case 'MOPED': return ['moped', '#075985', 'white']; case 'SCOOTER_SEATED': return ['seated_scooter', '#075985', 'white']; case 'SCOOTER_STANDING': return ['scooter', '#075985', 'white']; case 'OTHER': default: return ['other', '#075985', 'white']; } case 'RIDE_SHARING': return ['car', '#217edb', 'white']; case 'CAR': case 'CAR_PARKING': return ['car', '#4c4947', 'white']; case 'FLEX': case 'ODM': return ['taxi', '#fdb813', 'white']; case 'TRANSIT': case 'BUS': return ['bus', '#ff9800', 'white']; case 'COACH': return ['bus', '#9ccc65', 'black']; case 'TRAM': return ['tram', '#ebe717', 'white']; case 'SUBURBAN': return ['sbahn', '#4caf50', 'white']; case 'SUBWAY': return ['ubahn', '#3f51b5', 'white']; case 'FERRY': return ['ship', '#00acc1', 'white']; case 'AIRPLANE': return ['plane', '#90a4ae', 'white']; case 'HIGHSPEED_RAIL': return ['train', '#9c27b0', 'white']; case 'LONG_DISTANCE': return ['train', '#e91e63', 'white']; case 'NIGHT_RAIL': return ['train', '#1a237e', 'white']; case 'REGIONAL_FAST_RAIL': case 'REGIONAL_RAIL': case 'RAIL': return ['train', '#f44336', 'white']; case 'FUNICULAR': return ['funicular', '#795548', 'white']; case 'CABLE_CAR': return ['tram', '#795548', 'white']; case 'AERIAL_LIFT': return ['aerial_lift', '#795548', 'white']; } return ['train', '#000000', 'white']; }; export const getColor = (l: Colorable): [string, string] => { const [_, defaultColor, defaultTextColor] = getModeStyle(l); if (!l.routeColor) { return [defaultColor, defaultTextColor]; } return [`#${l.routeColor}`, `#${l.routeTextColor}`]; }; export const routeBorderColor = (l: Colorable) => { return `border-color: ${getColor(l)[0]}`; }; export const routeColor = (l: Colorable) => { const [color, textColor] = getColor(l); return `background-color: ${color}; color: ${textColor}; fill: ${textColor}`; }; ================================================ FILE: ui/src/lib/preprocessItinerary.ts ================================================ import type { Itinerary, Place, PlanResponse, Error as ApiError } from '@motis-project/motis-client'; import type { Location } from '$lib/Location'; import polyline from '@mapbox/polyline'; import type { RequestResult } from '@hey-api/client-fetch'; export const joinInterlinedLegs = (it: Itinerary) => { const joinedLegs = []; for (let i = 0; i < it.legs.length; i++) { if (it.legs[i].interlineWithPreviousLeg) { const pred = joinedLegs[joinedLegs.length - 1]; const curr = it.legs[i]; pred.intermediateStops!.push({ ...pred.to, switchTo: curr } as Place); pred.to = curr.to; pred.duration += curr.duration; pred.endTime = curr.endTime; pred.scheduledEndTime = curr.scheduledEndTime; pred.realTime ||= curr.realTime; pred.intermediateStops!.push(...curr.intermediateStops!); pred.legGeometry = { points: polyline.encode( [ ...polyline.decode(pred.legGeometry.points, pred.legGeometry.precision), ...polyline.decode(curr.legGeometry.points, curr.legGeometry.precision) ], pred.legGeometry.precision ), precision: pred.legGeometry.precision, length: pred.legGeometry.length + curr.legGeometry.length }; } else { joinedLegs.push(it.legs[i]); } } it.legs = joinedLegs; }; export const preprocessItinerary = (from: Location, to: Location) => { const updateItinerary = (it: Itinerary) => { if (it.legs[0].from.name === 'START') { it.legs[0].from.name = from.label!; } if (it.legs[it.legs.length - 1].to.name === 'END') { it.legs[it.legs.length - 1].to.name = to.label!; } joinInterlinedLegs(it); }; return (r: Awaited>): PlanResponse => { if (r.error) throw { error: r.error.error, status: r.response?.status }; r.data.itineraries.forEach(updateItinerary); r.data.direct.forEach(updateItinerary); return r.data; }; }; ================================================ FILE: ui/src/lib/toDateTime.ts ================================================ import { language } from './i18n/translation'; export const formatTime = (d: Date, timeZone: string | undefined): string => { return d.toLocaleTimeString(language, { hour: 'numeric', minute: 'numeric', timeZone, hour12: false }); }; export const formatDate = (d: Date, timeZone: string | undefined): string => { return d.toLocaleDateString(language, { day: 'numeric', month: 'numeric', year: 'numeric', timeZone }); }; export const formatDateTime = (d: Date, timeZone: string | undefined): string => { return d.toLocaleDateString(language, { day: 'numeric', month: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', timeZone }); }; export const getTz = (d: Date, timeZone: string | undefined): string | undefined => { const timeZoneOffset = new Intl.DateTimeFormat(language, { timeZone, timeZoneName: 'shortOffset' }) .formatToParts(d) .find((part) => part.type === 'timeZoneName')!.value; const isSameAsBrowserTimezone = new Intl.DateTimeFormat(language, { timeZoneName: 'shortOffset' }) .formatToParts(d) .find((part) => part.type === 'timeZoneName')!.value == timeZoneOffset; return isSameAsBrowserTimezone ? undefined : timeZoneOffset; }; ================================================ FILE: ui/src/lib/tripsWorker.ts ================================================ /// trips import polyline from '@mapbox/polyline'; import { client, trips, type Mode, type TripsData, type TripSegment } from '@motis-project/motis-client'; import type { Trip, TransferData, MetaData } from './types'; import type { QuerySerializerOptions } from '@hey-api/client-fetch'; import { getDelayColor, hexToRgb } from './Color'; import { getModeStyle, getColor } from './modeStyle'; //MATH const R = 6371; const TO_RAD = Math.PI / 180; const TO_DEG = 180 / Math.PI; const getSpacialData = ( path: Float64Array, i: number, segmentDistances: Float64Array, headings: Float32Array ) => { const i2 = i * 2; const i1 = i2 - 2; const lon2 = path[i2] * TO_RAD; const lat2 = path[i2 + 1] * TO_RAD; const lon1 = path[i1] * TO_RAD; const lat1 = path[i1 + 1] * TO_RAD; const dLon = lon2 - lon1; const dLat = lat2 - lat1; const cosLat1 = Math.cos(lat1); const cosLat2 = Math.cos(lat2); const sinLat1 = Math.sin(lat1); const sinLat2 = Math.sin(lat2); const cosDLon = Math.cos(dLon); // Haversine Distance const a = Math.sin(dLat / 2) ** 2 + cosLat1 * cosLat2 * Math.sin(dLon / 2) ** 2; const dist = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * R; segmentDistances[i - 1] = dist; // Bearing const y = Math.sin(dLon) * cosLat2; const x = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosDLon; headings[i - 1] = (-(Math.atan2(y, x) * TO_DEG + 360) % 360) + 90; }; //PROCESSING let status: number; const tripsMap = new Map(); const metaDataMap = new Map(); let metadata: MetaData[] = []; const fetchData = async (query: TripsData) => { const { data, response } = await trips(query); status = response.status; if (!data) return; tripsMap.clear(); metaDataMap.clear(); const tripBuilders = new Map< string, { paths: Float64Array[]; timestamps: Float64Array[]; headings: Float32Array[]; realtime: boolean; mode: Mode; routeColor?: string; departureDelay: number; arrivalDelay: number; } >(); for (const d of data) { const id = d.trips[0].tripId; const precision = 'precision' in query.query && typeof query.query.precision === 'number' ? query.query.precision : 5; const processed = processSegment(d, precision); if (!tripBuilders.has(id)) { tripBuilders.set(id, { paths: [processed.path], timestamps: [processed.timestamps], headings: [processed.headings], realtime: processed.realtime, mode: processed.mode, routeColor: processed.routeColor, departureDelay: processed.departureDelay, arrivalDelay: processed.arrivalDelay }); } else { const b = tripBuilders.get(id)!; b.paths.push(processed.path); b.timestamps.push(processed.timestamps); b.headings.push(processed.headings); } metaDataMap.set(id, { id, displayName: d.trips[0].displayName, tz: d.from.tz, from: d.from.name, to: d.to.name, realtime: d.realTime, arrival: d.arrival, departure: d.departure, scheduledArrival: d.scheduledArrival, scheduledDeparture: d.scheduledDeparture, departureDelay: processed.departureDelay, arrivalDelay: processed.arrivalDelay }); } tripBuilders.forEach((b, id) => { let pathLen = 0; let tsLen = 0; let hdLen = 0; for (let i = 0; i < b.paths.length; i++) { pathLen += b.paths[i].length; tsLen += b.timestamps[i].length; hdLen += b.headings[i].length; } const path = new Float64Array(pathLen); const timestamps = new Float64Array(tsLen); const headings = new Float32Array(hdLen); let pOff = 0, tOff = 0, hOff = 0; for (let i = 0; i < b.paths.length; i++) { path.set(b.paths[i], pOff); timestamps.set(b.timestamps[i], tOff); headings.set(b.headings[i], hOff); pOff += b.paths[i].length; tOff += b.timestamps[i].length; hOff += b.headings[i].length; } tripsMap.set(id, { realtime: b.realtime, mode: b.mode, routeColor: b.routeColor, path, timestamps, headings, currentIndx: 0, departureDelay: b.departureDelay, arrivalDelay: b.arrivalDelay }); }); metadata = Array.from(metaDataMap.values()); }; const processSegment = (s: TripSegment, precision: number): Trip => { const departure = new Date(s.departure).getTime(); const arrival = new Date(s.arrival).getTime(); const totalDuration = arrival - departure; const decoded = polyline.decode(s.polyline, precision); const count = decoded.length; const path = new Float64Array(count * 2); const timestamps = new Float64Array(count); const headings = new Float32Array(count); const segmentDistances = new Float64Array(count); let totalDistance = 0; for (let i = 0; i < count; i++) { const p = decoded[i]; const lon = p[1]; const lat = p[0]; path[i * 2] = lon; path[i * 2 + 1] = lat; if (i > 0) { getSpacialData(path, i, segmentDistances, headings); totalDistance += segmentDistances[i - 1]; } } if (count > 1) headings[count - 1] = headings[count - 2]; const invTotalDist = totalDistance === 0 ? 0 : 1 / totalDistance; let cumulativeDist = 0; for (let i = 0; i < count; i++) { timestamps[i] = departure + cumulativeDist * invTotalDist * totalDuration; cumulativeDist += segmentDistances[i]; } return { realtime: s.realTime, mode: s.mode, routeColor: s.routeColor, path, timestamps, headings, currentIndx: 0, departureDelay: departure - new Date(s.scheduledDeparture).getTime(), arrivalDelay: arrival - new Date(s.scheduledArrival).getTime() }; }; //STATE UPDATE function updateState(data: TransferData, colorMode: string) { let posIndex = 0; let colorIndex = 0; let angleIndex = 0; const time = Date.now(); let color; tripsMap.forEach((t) => { const stamps = t.timestamps; const path = t.path; const headings = t.headings; const len = stamps.length; switch (colorMode) { case 'rt': color = getDelayColor(t.departureDelay, t.realtime); break; case 'mode': color = hexToRgb(getModeStyle(t)[1]); break; case 'route': color = hexToRgb(getColor(t)[0]); break; case 'none': color = hexToRgb(getColor(t)[0]); break; } data.colors[colorIndex] = color![0]; data.colors[colorIndex + 1] = color![1]; data.colors[colorIndex + 2] = color![2]; let curr = t.currentIndx; while (curr < len - 1 && stamps[curr] < time) { curr++; } t.currentIndx = curr; const last = stamps[len - 1]; if (curr === 0) { data.positions[posIndex] = path[0]; data.positions[posIndex + 1] = path[1]; data.angles[angleIndex] = headings[0]; } else if (curr === len - 1 && time >= last) { const idx = 2 * (len - 1); data.positions[posIndex] = path[idx]; data.positions[posIndex + 1] = path[idx + 1]; data.angles[angleIndex] = headings[len - 1]; } else if (last > time) { const t0 = stamps[curr - 1]; const t1 = stamps[curr]; const r = (time - t0) / (t1 - t0); const prevIdx = 2 * (curr - 1); const nextIdx = 2 * curr; const x0 = path[prevIdx]; const y0 = path[prevIdx + 1]; const x1 = path[nextIdx]; const y1 = path[nextIdx + 1]; data.positions[posIndex] = x0 + (x1 - x0) * r; data.positions[posIndex + 1] = y0 + (y1 - y0) * r; data.angles[angleIndex] = headings[curr - 1]; } colorIndex += 3; angleIndex++; posIndex += 2; }); data.length = angleIndex; } //MESSAGING self.onmessage = async (e) => { switch (e.data.type) { case 'init': { const querySerializer = { array: { explode: false } } as QuerySerializerOptions; client.setConfig({ baseUrl: e.data.baseUrl, querySerializer }); break; } case 'fetch': await fetchData({ query: e.data.query }); postMessage({ type: 'fetch-complete', status }); break; case 'update': { const positions = new Float64Array(e.data.positions.buffer); const angles = new Float32Array(e.data.angles.buffer); const colors = new Uint8Array(e.data.colors.buffer); const hovIndex = e.data.index; const data = { colors, positions, angles, length: 0 }; updateState(data, e.data.colorMode); postMessage( { angles: data.angles, positions: data.positions, colors: data.colors, length: data.length, metadata: metadata[hovIndex], metadataIndex: hovIndex }, [data.angles.buffer, data.positions.buffer, data.colors.buffer] ); break; } } }; export {}; ================================================ FILE: ui/src/lib/types.ts ================================================ import type { Mode } from '@motis-project/motis-client'; export type Position = [number, number]; export type Trip = { realtime: boolean; routeColor?: string; mode: Mode; departureDelay: number; path: Float64Array; arrivalDelay: number; timestamps: Float64Array; currentIndx: number; headings: Float32Array; }; export type TransferData = { length: number; positions: Float64Array; angles: Float32Array; colors: Uint8Array; }; export type MetaData = { id: string; displayName?: string; tz?: string; from: string; to: string; realtime: boolean; arrival: string; departure: string; scheduledArrival: string; scheduledDeparture: string; departureDelay: number; arrivalDelay: number; }; ================================================ FILE: ui/src/lib/utils.ts ================================================ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { browser } from '$app/environment'; import { pushState, replaceState } from '$app/navigation'; import { page } from '$app/state'; import { trip } from '@motis-project/motis-client'; import { joinInterlinedLegs } from './preprocessItinerary'; import { language } from './i18n/translation'; import { tick } from 'svelte'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } const urlParams = browser ? new URLSearchParams(window.location.search) : undefined; export const getUrlArray = (key: string, defaultValue?: string[]): string[] => { if (urlParams) { const value = urlParams.get(key); if (value != null) { return value.split(',').filter((m) => m.length); } } if (defaultValue) { return defaultValue; } return []; }; export const preserveFromUrl = ( // eslint-disable-next-line queryParams: Record, field: string ) => { if (urlParams?.has(field)) { queryParams[field] = urlParams.get(field); } }; export const pushStateWithQueryString = async ( // eslint-disable-next-line queryParams: Record, newState: App.PageState, replace: boolean = false ) => { preserveFromUrl(queryParams, 'debug'); preserveFromUrl(queryParams, 'dark'); preserveFromUrl(queryParams, 'light'); preserveFromUrl(queryParams, 'motis'); preserveFromUrl(queryParams, 'language'); const params = new URLSearchParams(queryParams); const updateState = replace ? replaceState : pushState; try { updateState('?' + params.toString(), newState); } catch (e) { console.log(e); await tick(); updateState('?' + params.toString(), newState); } }; export const closeItinerary = () => { if (page.state.selectedStop) { onClickStop( page.state.selectedStop.name, page.state.selectedStop.stopId, page.state.selectedStop.time, page.state.stopArriveBy ?? false, true ); return; } pushStateWithQueryString({}, {}); }; export const onClickStop = ( name: string, stopId: string, time: Date, arriveBy: boolean = false, replace: boolean = false ) => { pushStateWithQueryString( { stopArriveBy: arriveBy, stopId, time: time.toISOString() }, { stopArriveBy: arriveBy, selectedStop: { name, stopId, time }, selectedItinerary: replace ? undefined : page.state.selectedItinerary, tripId: replace ? undefined : page.state.tripId, activeTab: 'departures' }, replace ); }; export const onClickTrip = async (tripId: string, replace: boolean = false) => { const { data: itinerary, error } = await trip({ query: { tripId, joinInterlinedLegs: false, language: [language] } }); if (error) { console.log(error); alert(String((error as Record).error?.toString() ?? error)); return; } joinInterlinedLegs(itinerary!); pushStateWithQueryString( { tripId }, { selectedItinerary: itinerary, tripId: tripId, selectedStop: replace ? undefined : page.state.selectedStop, activeTab: 'connections' }, replace ); }; ================================================ FILE: ui/src/routes/+layout.svelte ================================================ {@render children()} ================================================ FILE: ui/src/routes/+layout.ts ================================================ import { client } from '@motis-project/motis-client'; import { browser } from '$app/environment'; import type { QuerySerializerOptions } from '@hey-api/client-fetch'; export const prerender = true; if (browser) { const params = new URL(window.location.href).searchParams; const defaultProtocol = window.location.protocol; const defaultHost = window.location.hostname; const defaultPort = '8080'; const motisParam = params.get('motis'); let baseUrl = String(window.location.origin + window.location.pathname); if (motisParam) { if (/^[0-9]+$/.test(motisParam)) { baseUrl = defaultProtocol + '//' + defaultHost + ':' + motisParam; } else if (!motisParam.includes(':')) { baseUrl = defaultProtocol + '//' + motisParam + ':' + defaultPort; } else if (!motisParam.startsWith('http:') && !motisParam.startsWith('https:')) { baseUrl = defaultProtocol + '//' + motisParam; } else { baseUrl = motisParam; } } const querySerializer = { array: { explode: false } } as QuerySerializerOptions; client.setConfig({ baseUrl, querySerializer }); //`${window.location}` } ================================================ FILE: ui/src/routes/+page.svelte ================================================ {#snippet contextMenu(e: maplibregl.MapMouseEvent, close: CloseFn)} {#if activeTab == 'connections'} {:else if activeTab == 'isochrones'} {/if} {/snippet} {#snippet resultContent()} activeTab, (v) => { activeTab = v; pushState('', { activeTab: v }); } } class="max-w-full w-[520px] overflow-y-auto" > {t.connections} {t.departures} {t.isochrones.title} {#if activeTab == 'connections' && routingResponses.length !== 0 && !page.state.selectedItinerary} { pushState('', { selectedItinerary: selectedItinerary, scrollY: undefined, activeTab: 'connections' }); }} updateStartDest={preprocessItinerary(from, to)} /> {#if showMap && !page.state.selectedItinerary} {#each routingResponses as r, rI (rI)} {#await r then r} {#each r.itineraries as it, i (i)} { pushState('', { selectedItinerary: it, activeTab: 'connections' }); }} {level} {theme} /> {/each} {/await} {/each} {/if} {/if} {#if activeTab == 'connections' && page.state.selectedItinerary}

{t.journeyDetails}

{#if showMap} {/if} {/if} {#if activeTab == 'departures' && page.state.selectedStop}

{#if page.state.stopArriveBy} {t.arrivals} {:else} {t.departures} {/if} in {stopNameFromResponse}

{/if} {#if activeTab == 'isochrones' && one.match} {/if} {/snippet} {#if dataLoaded} {#if hasDebug} {/if} {#if browser} {#if isSmallScreen} {@render resultContent()} {:else}
{@render resultContent()}
{/if} {/if}
© OpenStreetMap {#if dataAttributionLink} | {t.timetableSources} {/if}
{#if showMap} {#if activeTab != 'isochrones'} {#if showRoutes} {#key routesOverlaySession} {/key} {/if} {/if} {#if from && activeTab == 'connections'} {/if} {#if stop && activeTab == 'departures'} {/if} {#if to && activeTab == 'connections'} {/if} {#if one && activeTab == 'isochrones'} {/if} {/if}
{/if} ================================================ FILE: ui/static/sprite_sdf.json ================================================ { "elevator": { "height": 26, "pixelRatio": 1, "width": 26, "x": 0, "y": 0, "sdf": true } } ================================================ FILE: ui/static/sprite_sdf@2x.json ================================================ { "elevator": { "height": 52, "pixelRatio": 2, "width": 52, "x": 0, "y": 0, "sdf": true } } ================================================ FILE: ui/svelte.config.js ================================================ import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://kit.svelte.dev/docs/integrations#preprocessors // for more information about preprocessors preprocess: vitePreprocess(), kit: { inlineStyleThreshold: 5000, adapter: adapter({ // default options are shown. On some platforms // these options are set automatically — see below pages: 'build', assets: 'build', fallback: undefined, precompress: false, strict: true }) } }; export default config; ================================================ FILE: ui/tailwind.config.js ================================================ import { fontFamily } from 'tailwindcss/defaultTheme'; import tailwindcssAnimate from 'tailwindcss-animate'; /** @type {import('tailwindcss').Config} */ const config = { darkMode: ['class'], content: ['./src/**/*.{html,js,svelte,ts}'], safelist: ['dark'], theme: { container: { center: true, padding: '2rem', screens: { '2xl': '1400px' } }, extend: { colors: { border: 'hsl(var(--border) / )', input: 'hsl(var(--input) / )', ring: 'hsl(var(--ring) / )', background: 'hsl(var(--background) / )', foreground: 'hsl(var(--foreground) / )', primary: { DEFAULT: 'hsl(var(--primary) / )', foreground: 'hsl(var(--primary-foreground) / )' }, secondary: { DEFAULT: 'hsl(var(--secondary) / )', foreground: 'hsl(var(--secondary-foreground) / )' }, destructive: { DEFAULT: 'hsl(var(--destructive) / )', foreground: 'hsl(var(--destructive-foreground) / )' }, muted: { DEFAULT: 'hsl(var(--muted) / )', foreground: 'hsl(var(--muted-foreground) / )' }, accent: { DEFAULT: 'hsl(var(--accent) / )', foreground: 'hsl(var(--accent-foreground) / )' }, popover: { DEFAULT: 'hsl(var(--popover) / )', foreground: 'hsl(var(--popover-foreground) / )' }, card: { DEFAULT: 'hsl(var(--card) / )', foreground: 'hsl(var(--card-foreground) / )' }, sidebar: { DEFAULT: 'hsl(var(--sidebar-background))', foreground: 'hsl(var(--sidebar-foreground))', primary: 'hsl(var(--sidebar-primary))', 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', accent: 'hsl(var(--sidebar-accent))', 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', border: 'hsl(var(--sidebar-border))', ring: 'hsl(var(--sidebar-ring))' } }, borderRadius: { xl: 'calc(var(--radius) + 4px)', lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' }, fontFamily: { sans: [...fontFamily.sans] }, keyframes: { 'accordion-down': { from: { height: '0' }, to: { height: 'var(--bits-accordion-content-height)' } }, 'accordion-up': { from: { height: 'var(--bits-accordion-content-height)' }, to: { height: '0' } }, 'caret-blink': { '0%,70%,100%': { opacity: '1' }, '20%,50%': { opacity: '0' } } }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', 'caret-blink': 'caret-blink 1.25s ease-out infinite' } } }, plugins: [tailwindcssAnimate] }; export default config; ================================================ FILE: ui/tests/test.ts ================================================ import { expect, test } from '@playwright/test'; test('index page has expected h1', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible(); }); ================================================ FILE: ui/tsconfig.json ================================================ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "moduleResolution": "bundler" }, "exclude": [ "../node_modules/**", "../src/service-worker.js", "../src/service-worker/**/*.js", "../src/service-worker.ts", "../src/service-worker/**/*.ts", "../src/service-worker.d.ts", "../src/service-worker/**/*.d.ts", "api/dist/**", "api/node_modules/**" ] // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // from the referenced tsconfig.json - TypeScript does not merge them in } ================================================ FILE: ui/vite.config.ts ================================================ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [sveltekit()], test: { include: ['src/**/*.{test,spec}.{js,ts}'] }, server: { fs: { strict: false } }, build: { sourcemap: true, rollupOptions: { output: { manualChunks: (id) => { if (id.includes('node_modules')) { if (id.includes('deck.gl')) return 'deck-vendor'; if (id.includes('svelte')) return 'svelte-vendor'; if (id.includes('luma.gl')) return 'luma-vendor'; if (id.includes('.gl')) return 'gl-vendor'; return 'other-vendor'; } } } } } });