[
  {
    "path": ".clang-format",
    "content": "BasedOnStyle: Google\nIndentWidth: 2\nLanguage: Cpp\nDerivePointerAlignment: false\nPointerAlignment: Left\nAccessModifierOffset: -2\nConstructorInitializerAllOnOneLineOrOnePerLine: true\nAlignTrailingComments: false\nKeepEmptyLinesAtTheStartOfBlocks: true\nAllowShortCaseLabelsOnASingleLine: true\nAlwaysBreakTemplateDeclarations: true\nSpacesBeforeTrailingComments: 2\nSortIncludes: true\nIncludeBlocks: Preserve\nBinPackParameters: false\nQualifierAlignment: Right\nIncludeCategories:\n  - Regex: '^<.*>'\n    Priority: 1\n  - Regex: '^\"boost.*'\n    Priority: 2\n  - Regex: '^\"nigiri/core/common.*'\n    Priority: 3\n  - Regex: '^\"nigiri/core/schedule.*'\n    Priority: 4\n  - Regex: '^\"nigiri/core/.*'\n    Priority: 5\n  - Regex: '^\"nigiri/module/.*'\n    Priority: 6\n  - Regex: '^\"nigiri/bootstrap/.*'\n    Priority: 7\n  - Regex: '^\"nigiri/loader/.*'\n    Priority: 8\n  - Regex: '^\"nigiri/.*'\n    Priority: 9\n  - Regex: '^\"nigiri/protocol.*'\n    Priority: 10\n  - Regex: '^\"./.*'\n    Priority: 11\n  - Regex: '.*'\n    Priority: 12\n---\nBasedOnStyle: Google\nIndentWidth: 2\nLanguage: ObjC\nDerivePointerAlignment: false\nPointerAlignment: Left\nAccessModifierOffset: -2\nConstructorInitializerAllOnOneLineOrOnePerLine: true\nAlignTrailingComments: false\nKeepEmptyLinesAtTheStartOfBlocks: true\nAllowShortCaseLabelsOnASingleLine: true\nAlwaysBreakTemplateDeclarations: true\nSpacesBeforeTrailingComments: 2\nColumnLimit: 80\nQualifierAlignment: Right\nIncludeCategories:\n  - Regex: '^<.*>'\n    Priority: 1\n  - Regex: '^\"boost.*'\n    Priority: 2\n  - Regex: '^\"nigiri/core/common.*'\n    Priority: 3\n  - Regex: '^\"nigiri/core/schedule.*'\n    Priority: 4\n  - Regex: '^\"nigiri/core/.*'\n    Priority: 5\n  - Regex: '^\"nigiri/module/.*'\n    Priority: 6\n  - Regex: '^\"nigiri/bootstrap/.*'\n    Priority: 7\n  - Regex: '^\"nigiri/loader/.*'\n    Priority: 8\n  - Regex: '^\"nigiri/.*'\n    Priority: 9\n  - Regex: '^\"nigiri/protocol.*'\n    Priority: 10\n  - Regex: '^\"./.*'\n    Priority: 11\n  - Regex: '.*'\n    Priority: 12\n"
  },
  {
    "path": ".clang-tidy.in",
    "content": "Checks: \"*,\\\n-llvmlibc-*,\\\n-abseil-*,\\\n-readability-identifier-length,\\\n-altera-unroll-loops,\\\n-altera-id-dependent-backward-branch,\\\n-bugprone-easily-swappable-parameters,\\\n-bugprone-implicit-widening-of-multiplication-result,\\\n-llvm-else-after-return,\\\n-hicpp-named-parameter,\\\n-cert-err60-cpp,\\\n-clang-analyzer-core.NonNullParamChecker,\\\n-misc-unused-parameters,\\\n-cppcoreguidelines-pro-bounds-array-to-pointer-decay,\\\n-cppcoreguidelines-pro-bounds-pointer-arithmetic,\\\n-cppcoreguidelines-pro-type-union-access,\\\n-readability-simplify-boolean-expr,\\\n-clang-analyzer-alpha*,\\\n-google-build-using-namespace,\\\n-clang-analyzer-optin.osx*,\\\n-clang-analyzer-osx*,\\\n-readability-implicit-bool-cast,\\\n-readability-else-after-return,\\\n-llvm-include-order,\\\n-clang-analyzer-alpha.unix.PthreadLock,\\\n-llvm-header-guard,\\\n-readability-named-parameter,\\\n-clang-analyzer-alpha.deadcode.UnreachableCode,\\\n-cppcoreguidelines-pro-type-reinterpret-cast,\\\n-cppcoreguidelines-pro-type-vararg,\\\n-misc-move-const-arg,\\\n-google-runtime-references,\\\n-cert-err58-cpp,\\\n-modernize-use-default-member-init,\\\n-fuchsia-overloaded-operator,\\\n-fuchsia-default-arguments,\\\n-hicpp-vararg,\\\n-clang-analyzer-optin.cplusplus.VirtualCall,\\\n-cppcoreguidelines-owning-memory,\\\n-hicpp-no-array-decay,\\\n-*-magic-numbers,\\\n-*-non-private-member-variables-in-classes,\\\n-fuchsia-statically-constructed-objects,\\\n-readability-isolate-declaration,\\\n-fuchsia-multiple-inheritance,\\\n-fuchsia-trailing-return,\\\n-portability-simd-intrinsics,\\\n-modernize-use-nodiscard,\\\n-cppcoreguidelines-pro-bounds-constant-array-index,\\\n-*-avoid-c-arrays,\\\n-*-narrowing-conversions,\\\n-*-avoid-goto,\\\n-hicpp-multiway-paths-covered,\\\n-clang-analyzer-cplusplus.NewDeleteLeaks,\\\n-clang-analyzer-cplusplus.NewDelete,\\\n-hicpp-signed-bitwise,\\\n-cert-msc32-c,\\\n-cert-msc51-cpp,\\\n-bugprone-exception-escape,\\\n-cppcoreguidelines-macro-usage,\\\n-cert-dcl21-cpp,\\\n-modernize-use-trailing-return-type,\\\n-fuchsia-default-arguments-calls,\\\n-fuchsia-default-arguments-declarations,\\\n-misc-no-recursion,\\\n-llvmlibc-callee-namespace,\\\n-llvm-else-after-return,\\\n-llvm-qualified-auto,\\\n-readability-qualified-auto,\\\n-google-readability-avoid-underscore-in-googletest-name,\\\n-readability-function-cognitive-complexity,\\\n-readability-avoid-const-params-in-decls,\\\n-cppcoreguidelines-avoid-const-or-ref-data-members,\\\n-cppcoreguidelines-avoid-do-while,\\\n-altera-struct-pack-align,\\\n-bugprone-unchecked-optional-access,\\\n-readability-identifier-naming,\\\n-cert-dcl37-c,\\\n-bugprone-reserved-identifier,\\\n-cert-dcl51-cpp,\\\n-misc-confusable-identifiers,\\\n-clang-analyzer-optin.core.EnumCastOutOfRange,\\\n-clang-analyzer-core.CallAndMessage\"\nWarningsAsErrors: '*'\nHeaderFilterRegex: '^${RELATIVE_SOURCE_DIR}(base|modules|test)/'\nAnalyzeTemporaryDtors: false\nUseColor: true\nUser:            root\nCheckOptions:\n  - key:             cert-err61-cpp.CheckThrowTemporaries\n    value:           '1'\n  - key:             cert-oop11-cpp.IncludeStyle\n    value:           llvm\n  - key:             cppcoreguidelines-pro-bounds-constant-array-index.GslHeader\n    value:           ''\n  - key:             cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle\n    value:           '0'\n  - key:             google-readability-braces-around-statements.ShortStatementLines\n    value:           '1'\n  - key:             google-readability-function-size.BranchThreshold\n    value:           '4294967295'\n  - key:             google-readability-function-size.LineThreshold\n    value:           '4294967295'\n  - key:             google-readability-function-size.StatementThreshold\n    value:           '800'\n  - key:             google-readability-namespace-comments.ShortNamespaceLines\n    value:           '10'\n  - key:             google-readability-namespace-comments.SpacesBeforeComments\n    value:           '2'\n  - key:             google-runtime-int.SignedTypePrefix\n    value:           int\n  - key:             google-runtime-int.TypeSuffix\n    value:           ''\n  - key:             google-runtime-int.UnsignedTypePrefix\n    value:           uint\n  - key:             llvm-namespace-comment.ShortNamespaceLines\n    value:           '1'\n  - key:             llvm-namespace-comment.SpacesBeforeComments\n    value:           '2'\n  - key:             misc-assert-side-effect.AssertMacros\n    value:           assert\n  - key:             misc-assert-side-effect.CheckFunctionCalls\n    value:           '0'\n  - key:             misc-definitions-in-headers.UseHeaderFileExtension\n    value:           '1'\n  - key:             misc-move-constructor-init.IncludeStyle\n    value:           llvm\n  - key:             misc-throw-by-value-catch-by-reference.CheckThrowTemporaries\n    value:           '1'\n  - key:             modernize-loop-convert.MaxCopySize\n    value:           '16'\n  - key:             modernize-loop-convert.MinConfidence\n    value:           reasonable\n  - key:             modernize-loop-convert.NamingStyle\n    value:           lower_case\n  - key:             modernize-pass-by-value.IncludeStyle\n    value:           llvm\n  - key:             modernize-replace-auto-ptr.IncludeStyle\n    value:           llvm\n  - key:             modernize-use-nullptr.NullMacros\n    value:           'NULL'\n  - key:             readability-braces-around-statements.ShortStatementLines\n    value:           '0'\n  - key:             readability-function-size.BranchThreshold\n    value:           '4294967295'\n  - key:             readability-function-size.LineThreshold\n    value:           '4294967295'\n  - key:             readability-function-size.StatementThreshold\n    value:           '800'\n  - key:             readability-identifier-naming.AbstractClassCase\n    value:           lower_case\n  - key:             readability-identifier-naming.AbstractClassPrefix\n    value:           ''\n  - key:             readability-identifier-naming.AbstractClassSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ClassCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ClassConstantCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ClassConstantPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ClassConstantSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ClassMemberCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ClassMemberPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ClassMemberSuffix\n    value:           '_'\n  - key:             readability-identifier-naming.ClassMethodCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ClassMethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ClassMethodSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ClassPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ClassSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ConstantCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ConstantMemberCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ConstantMemberPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ConstantMemberSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ConstantParameterCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ConstantParameterPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ConstantParameterSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ConstantPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ConstantSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ConstexprFunctionCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ConstexprFunctionPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ConstexprFunctionSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ConstexprMethodCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ConstexprMethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ConstexprMethodSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ConstexprVariableCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.ConstexprVariablePrefix\n    value:           ''\n  - key:             readability-identifier-naming.ConstexprVariableSuffix\n    value:           ''\n  - key:             readability-identifier-naming.EnumCase\n    value:           lower_case\n  - key:             readability-identifier-naming.EnumConstantCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.EnumConstantPrefix\n    value:           ''\n  - key:             readability-identifier-naming.EnumConstantSuffix\n    value:           ''\n  - key:             readability-identifier-naming.EnumPrefix\n    value:           ''\n  - key:             readability-identifier-naming.EnumSuffix\n    value:           ''\n  - key:             readability-identifier-naming.FunctionCase\n    value:           lower_case\n  - key:             readability-identifier-naming.FunctionPrefix\n    value:           ''\n  - key:             readability-identifier-naming.FunctionSuffix\n    value:           ''\n  - key:             readability-identifier-naming.GlobalConstantCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.GlobalConstantPrefix\n    value:           ''\n  - key:             readability-identifier-naming.GlobalConstantSuffix\n    value:           ''\n  - key:             readability-identifier-naming.GlobalFunctionCase\n    value:           lower_case\n  - key:             readability-identifier-naming.GlobalFunctionPrefix\n    value:           ''\n  - key:             readability-identifier-naming.GlobalFunctionSuffix\n    value:           ''\n  - key:             readability-identifier-naming.GlobalVariableCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.GlobalVariablePrefix\n    value:           ''\n  - key:             readability-identifier-naming.GlobalVariableSuffix\n    value:           ''\n  - key:             readability-identifier-naming.IgnoreFailedSplit\n    value:           '0'\n  - key:             readability-identifier-naming.InlineNamespaceCase\n    value:           lower_case\n  - key:             readability-identifier-naming.InlineNamespacePrefix\n    value:           ''\n  - key:             readability-identifier-naming.InlineNamespaceSuffix\n    value:           ''\n  - key:             readability-identifier-naming.LocalConstantCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.LocalConstantPrefix\n    value:           ''\n  - key:             readability-identifier-naming.LocalConstantSuffix\n    value:           ''\n  - key:             readability-identifier-naming.LocalVariableCase\n    value:           lower_case\n  - key:             readability-identifier-naming.LocalVariablePrefix\n    value:           ''\n  - key:             readability-identifier-naming.LocalVariableSuffix\n    value:           ''\n  - key:             readability-identifier-naming.MemberCase\n    value:           lower_case\n  - key:             readability-identifier-naming.MemberPrefix\n    value:           ''\n  - key:             readability-identifier-naming.MemberSuffix\n    value:           '_'\n  - key:             readability-identifier-naming.MethodCase\n    value:           lower_case\n  - key:             readability-identifier-naming.MethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.MethodSuffix\n    value:           ''\n  - key:             readability-identifier-naming.NamespaceCase\n    value:           lower_case\n  - key:             readability-identifier-naming.NamespacePrefix\n    value:           ''\n  - key:             readability-identifier-naming.NamespaceSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ParameterCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ParameterPackCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ParameterPackPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ParameterPackSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ParameterPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ParameterSuffix\n    value:           ''\n  - key:             readability-identifier-naming.PrivateMemberCase\n    value:           lower_case\n  - key:             readability-identifier-naming.PrivateMemberPrefix\n    value:           ''\n  - key:             readability-identifier-naming.PrivateMemberSuffix\n    value:           '_'\n  - key:             readability-identifier-naming.PrivateMethodCase\n    value:           lower_case\n  - key:             readability-identifier-naming.PrivateMethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.PrivateMethodSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ProtectedMemberCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ProtectedMemberPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ProtectedMemberSuffix\n    value:           '_'\n  - key:             readability-identifier-naming.ProtectedMethodCase\n    value:           lower_case\n  - key:             readability-identifier-naming.ProtectedMethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ProtectedMethodSuffix\n    value:           ''\n  - key:             readability-identifier-naming.PublicMemberCase\n    value:           lower_case\n  - key:             readability-identifier-naming.PublicMemberPrefix\n    value:           ''\n  - key:             readability-identifier-naming.PublicMemberSuffix\n    value:           '_'\n  - key:             readability-identifier-naming.PublicMethodCase\n    value:           lower_case\n  - key:             readability-identifier-naming.PublicMethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.PublicMethodSuffix\n    value:           ''\n  - key:             readability-identifier-naming.StaticConstantCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.StaticConstantPrefix\n    value:           ''\n  - key:             readability-identifier-naming.StaticConstantSuffix\n    value:           ''\n  - key:             readability-identifier-naming.StaticVariableCase\n    value:           lower_case\n  - key:             readability-identifier-naming.StaticVariablePrefix\n    value:           ''\n  - key:             readability-identifier-naming.StaticVariableSuffix\n    value:           ''\n  - key:             readability-identifier-naming.StructCase\n    value:           lower_case\n  - key:             readability-identifier-naming.StructPrefix\n    value:           ''\n  - key:             readability-identifier-naming.StructSuffix\n    value:           ''\n  - key:             readability-identifier-naming.TemplateParameterCase\n    value:           CamelCase\n  - key:             readability-identifier-naming.TemplateTemplateParameterCase\n    value:           CamelCase\n  - key:             readability-identifier-naming.TypedefCase\n    value:           lower_case\n  - key:             readability-identifier-naming.TypedefPrefix\n    value:           ''\n  - key:             readability-identifier-naming.TypedefSuffix\n    value:           ''\n  - key:             readability-identifier-naming.UnionCase\n    value:           lower_case\n  - key:             readability-identifier-naming.UnionPrefix\n    value:           ''\n  - key:             readability-identifier-naming.UnionSuffix\n    value:           ''\n  - key:             readability-identifier-naming.ValueTemplateParameterCase\n    value:           CamelCase\n  - key:             readability-identifier-naming.ValueTemplateParameterPrefix\n    value:           ''\n  - key:             readability-identifier-naming.ValueTemplateParameterSuffix\n    value:           ''\n  - key:             readability-identifier-naming.VariableCase\n    value:           aNy_CasE\n  - key:             readability-identifier-naming.VariablePrefix\n    value:           ''\n  - key:             readability-identifier-naming.VariableSuffix\n    value:           ''\n  - key:             readability-identifier-naming.VirtualMethodCase\n    value:           lower_case\n  - key:             readability-identifier-naming.VirtualMethodPrefix\n    value:           ''\n  - key:             readability-identifier-naming.VirtualMethodSuffix\n    value:           ''\n  - key:             readability-simplify-boolean-expr.ChainedConditionalAssignment\n    value:           '0'\n  - key:             readability-simplify-boolean-expr.ChainedConditionalReturn\n    value:           '0'\n  - key:             performance-for-range-copy.AllowedTypes\n    value:           'offset_ptr;ptr'\n  - key:             performance-unnecessary-value-param.AllowedTypes\n    value:           'offset_ptr;ptr'\n  - key:             readability-identifier-naming.TypeTemplateParameterIgnoredRegexp\n    value:           'expr-type'\n  - key:             readability-identifier-naming.TemplateParameterIgnoredRegexp\n    value:           'expr-type'\n  - key:             readability-identifier-naming.TypeTemplateParameterIgnoredRegexp\n    value:           'expr-type'\n  - key:             readability-identifier-naming.TemplateTemplateParameterIgnoredRegexp\n    value:           'expr-type'\n  - key:             readability-identifier-naming.ValueTemplateParameterIgnoredRegexp\n    value:           'expr-type'\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n  release:\n    types:\n      - published\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  openapi-validate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 24\n\n      - name: Install Dependencies\n        run: npm install @openapitools/openapi-generator-cli -g\n\n      - name: OpenAPI Lint\n        run: openapi-generator-cli validate -i openapi.yaml\n\n  ui:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 24\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: Install Dependencies\n        working-directory: ui\n        run: pnpm install\n\n      - name: Build\n        working-directory: ui\n        run: pnpm -r build\n\n      - name: Code Lint\n        working-directory: ui\n        run: pnpm run lint\n\n      - name: Svelte Check\n        working-directory: ui\n        run: pnpm run check\n\n  formatting:\n    runs-on: ubuntu-latest\n    container: ghcr.io/motis-project/docker-cpp-build\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Format files\n        run: |\n          find include src test \\\n            -type f -a \\( -name \"*.cc\" -o -name \"*.h\" -o -name \".cuh\" -o -name \".cu\" \\) \\\n            -print0 | xargs -0 clang-format-21 -i\n\n      - name: Check for differences\n        run: |\n          git config --global --add safe.directory `pwd`\n          git status --porcelain\n          git status --porcelain | xargs -I {} -0 test -z \\\"{}\\\"\n\n  msvc:\n    runs-on: [ self-hosted, windows, x64 ]\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - mode: Debug\n          - mode: Release\n    env:\n      CXX: cl.exe\n      CC: cl.exe\n      BUILDCACHE_COMPRESS: true\n      BUILDCACHE_DIRECT_MODE: true\n      BUILDCACHE_ACCURACY: SLOPPY # not suitable for coverage/debugging\n      BUILDCACHE_DIR: ${{ github.workspace }}/.buildcache\n      BUILDCACHE_LUA_PATH: ${{ github.workspace }}/tools\n      BUILDCACHE_MAX_CACHE_SIZE: 1073741824\n      CLICOLOR_FORCE: 1\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 24\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n          dest: \"~/setup-pnpm-${{ matrix.config.mode }}\"\n      - uses: ilammy/msvc-dev-cmd@v1\n\n      # ==== RESTORE CACHE ====\n      - name: Restore buildcache Cache\n        run: |\n          $buildcachePath = \"${{ runner.tool_cache }}\\${{ github.event.repository.name }}\\buildcache-${{ matrix.config.mode }}\"\n          New-Item -ItemType Directory -Force -Path $buildcachePath\n          New-Item -Path ${{ github.workspace }}/.buildcache -ItemType SymbolicLink -Value $buildcachePath\n\n      - name: Restore Dependencies Cache\n        run: |\n          $depsPath = \"${{ runner.tool_cache }}\\${{ github.event.repository.name }}\\deps\"\n          New-Item -ItemType Directory -Force -Path $depsPath\n          New-Item -Path ${{ github.workspace }}\\deps\\ -ItemType SymbolicLink -Value $depsPath\n\n      - name: Build\n        run: |\n          cmake `\n            -GNinja -S . -B build `\n            -DCMAKE_BUILD_TYPE=${{ matrix.config.mode }} `\n            -DMOTIS_MIMALLOC=ON\n          .\\build\\buildcache\\bin\\buildcache.exe -z\n          cmake --build build --target motis motis-test motis-web-ui\n          $CompilerExitCode = $LastExitCode\n          Copy-Item ${env:VCToolsRedistDir}x64\\Microsoft.VC143.CRT\\*.dll .\\build\\\n          .\\build\\buildcache\\bin\\buildcache.exe -s\n          exit $CompilerExitCode\n\n      # ==== TESTS ====\n      - name: Run Tests\n        run: .\\build\\motis-test.exe\n\n      # ==== DISTRIBUTION ====\n      - name: Move Profiles\n        if: matrix.config.mode == 'Release'\n        run: |\n          mkdir dist\n          Copy-Item .\\deps\\tiles\\profile dist\\tiles-profiles -Recurse\n          mv .\\build\\motis.exe dist\n          mv .\\build\\*.dll dist\n          mv .\\ui\\build dist\\ui\n          cd dist\n          7z a motis-windows.zip *\n          mv motis-windows.zip ..\n\n      - name: Upload Distribution\n        if: matrix.config.mode == 'Release'\n        uses: actions/upload-artifact@v4\n        with:\n          name: motis-windows\n          path: dist\n\n      # ==== RELEASE ====\n      - name: Upload Release\n        if: github.event.action == 'published' && matrix.config.mode == 'Release'\n        uses: actions/upload-release-asset@v1.0.2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ github.event.release.upload_url }}\n          asset_path: ./motis-windows.zip\n          asset_name: motis-windows.zip\n          asset_content_type: application/zip\n\n  macos:\n    runs-on: macos-latest\n    env:\n      BUILDCACHE_COMPRESS: true\n      BUILDCACHE_DIRECT_MODE: true\n      BUILDCACHE_ACCURACY: SLOPPY\n      BUILDCACHE_LUA_PATH: ${{ github.workspace }}/tools\n      BUILDCACHE_DIR: ${{ github.workspace }}/.buildcache\n      UBSAN_OPTIONS: halt_on_error=1:abort_on_error=1\n      ASAN_OPTIONS: alloc_dealloc_mismatch=0\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: maxim-lobanov/setup-xcode@v1\n        with:\n          xcode-version: 16.2\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: Install ninja\n        run: brew install ninja\n\n      # ==== RESTORE CACHE ====\n      - name: Restore buildcache Cache\n        uses: actions/cache/restore@v4\n        id: restore-buildcache\n        with:\n          path: ${{ github.workspace }}/.buildcache\n          key: buildcache-${{ hashFiles('.pkg') }}-${{ hashFiles('**/*.h') }}-${{ hashFiles('**/*.cc') }}\n          restore-keys: |\n            buildcache-${{ hashFiles('.pkg') }}-${{ hashFiles('**/*.h') }}-\n            buildcache-${{ hashFiles('.pkg') }}-\n            buildcache-\n\n      - name: Dependencies Cache\n        uses: actions/cache/restore@v4\n        id: restore-deps\n        with:\n          path: ${{ github.workspace }}/deps\n          key: deps-${{ hashFiles('.pkg') }}\n          restore-keys: deps-\n\n      # ==== BUILD ====\n      - name: CMake\n        run: cmake -G Ninja -S . -B build --preset=macos-arm64\n\n      - name: Build\n        run: |\n          ./build/buildcache/bin/buildcache -z\n          cmake --build build --target motis motis-test motis-web-ui\n          ./build/buildcache/bin/buildcache -s\n\n      # ==== TESTS ====\n      - name: Run Tests\n        if: matrix.config.tests == 'On'\n        run: build/motis-test\n\n      # ==== SAVE CACHE ====\n      - name: Save buildcache cache\n        if: always()\n        uses: actions/cache/save@v4\n        with:\n          path: ${{ github.workspace }}/.buildcache\n          key: ${{ steps.restore-buildcache.outputs.cache-primary-key }}\n\n      - name: Save deps cache\n        if: always()\n        uses: actions/cache/save@v4\n        with:\n          path: ${{ github.workspace }}/deps\n          key: ${{ steps.restore-deps.outputs.cache-primary-key }}\n\n      # ==== DISTRIBUTION ====\n      - name: Create Distribution\n        run: |\n          mkdir motis\n          mv build/motis motis/motis\n          mv ui/build motis/ui\n          cp -r deps/tiles/profile motis/tiles-profiles\n          tar -C ./motis -cjf motis-macos-arm64.tar.bz2 ./motis ./tiles-profiles ./ui\n\n      - name: Upload Distribution\n        uses: actions/upload-artifact@v4\n        with:\n          name: motis-macos-arm64\n          path: motis-macos-arm64.tar.bz2\n\n      # ==== RELEASE ====\n      - name: Upload Release\n        if: github.event.action == 'published'\n        uses: actions/upload-release-asset@v1.0.2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ github.event.release.upload_url }}\n          asset_path: ./motis-macos-arm64.tar.bz2\n          asset_name: motis-macos-arm64.tar.bz2\n          asset_content_type: application/x-tar\n\n  linux:\n    runs-on: [ self-hosted, linux, x64, '${{ matrix.config.preset }}' ]\n    container:\n      image: ghcr.io/motis-project/docker-cpp-build\n      volumes:\n        - ${{ github.event.repository.name }}-${{ matrix.config.preset }}-deps:/deps\n        - ${{ github.event.repository.name }}-${{ matrix.config.preset }}-buildcache:/buildcache\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - preset: linux-amd64-release\n            artifact: linux-amd64\n          - preset: linux-arm64-release\n            artifact: linux-arm64\n          - preset: linux-sanitizer\n          - preset: linux-debug\n            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\n    env:\n      BUILDCACHE_DIR: /buildcache\n      BUILDCACHE_DIRECT_MODE: true\n      BUILDCACHE_MAX_CACHE_SIZE: 26843545600\n      BUILDCACHE_LUA_PATH: ${{ github.workspace }}/tools\n      UBSAN_OPTIONS: halt_on_error=1:abort_on_error=1\n      ASAN_OPTIONS: alloc_dealloc_mismatch=0\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n\n      - name: Get deps\n        run: ln -s /deps deps\n\n      - name: CMake\n        run: |\n          git config --global --add safe.directory `pwd`\n          cmake -G Ninja -S . -B build --preset=${{ matrix.config.preset }}\n\n      # ==== BUILD ====\n      - name: Build\n        run: |\n          buildcache -z\n          cmake --build build --target motis motis-test motis-web-ui\n          buildcache -s\n\n      # ==== TESTS ====\n      - name: Run Integration Tests\n        if: matrix.config.preset != 'linux-arm64-release'\n        run: ${{ matrix.config.emulator }} build/motis-test\n\n      # ==== FULL DATASET TEST ====\n      - name: Test Full Dataset\n        if: matrix.config.preset != 'linux-debug' && matrix.config.preset != 'linux-arm64-release'\n        run: |\n          ln -s deps/tiles/profile tiles-profiles\n          wget -N https://github.com/motis-project/test-data/raw/aachen/aachen.osm.pbf\n          wget -N https://github.com/motis-project/test-data/raw/aachen/AVV_GTFS_Masten_mit_SPNV.zip\n          ${{ matrix.config.emulator }} ./build/motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip\n          yq -Y -i '.timetable *= {\n              \"route_shapes\": {\n                \"missing_shapes\": true,\n                \"replace_shapes\": true,\n                \"clasz\": { \"COACH\": false }\n              }\n          }' config.yml\n          ${{ matrix.config.emulator }} ./build/motis import\n          ${{ matrix.config.emulator }} ./build/motis generate -n 10\n          ${{ matrix.config.emulator }} ./build/motis batch\n          ${{ matrix.config.emulator }} ./build/motis compare -q queries.txt -r responses.txt responses.txt\n\n      - name: Test bin_ver compatibility with previous release\n        if: matrix.config.preset == 'linux-amd64-release' && github.event.action != 'published'\n        run: |\n          wget -N https://github.com/motis-project/motis/releases/latest/download/motis-linux-amd64.tar.bz2\n          tar -xjf motis-linux-amd64.tar.bz2 ./motis\n          ${{ matrix.config.emulator }} ./motis import\n          ${{ matrix.config.emulator }} ./build/motis import\n          rm ./motis\n\n      # ==== DISTRIBUTION ====\n      - name: Create Distribution\n        if: matrix.config.artifact\n        run: |\n          mkdir motis\n          mv build/motis motis/motis\n          mv ui/build motis/ui\n          cp -r deps/tiles/profile motis/tiles-profiles\n          tar -C ./motis -cjf motis-${{ matrix.config.artifact }}.tar.bz2 ./motis ./tiles-profiles ./ui\n\n      - name: Upload Distribution\n        if: matrix.config.artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: motis-${{ matrix.config.artifact }}\n          path: motis-${{ matrix.config.artifact }}.tar.bz2\n\n      # ==== RELEASE ====\n      - name: Upload Release\n        if: github.event.action == 'published' && matrix.config.artifact\n        uses: actions/upload-release-asset@v1.0.2\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ github.event.release.upload_url }}\n          asset_path: ./motis-${{ matrix.config.artifact }}.tar.bz2\n          asset_name: motis-${{ matrix.config.artifact }}.tar.bz2\n          asset_content_type: application/x-\n\n  docker:\n    if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}\n    runs-on: ubuntu-latest\n    needs: linux\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Download artifacts\n        uses: actions/download-artifact@v4\n\n      - name: Docker setup-buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          install: true\n\n      - name: Docker Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            ghcr.io/${{ github.repository }}\n          tags: |\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=edge\n\n      - name: Docker build and push\n        uses: docker/build-push-action@v5\n        with:\n          push: true\n          context: .\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n\n  publish-motis-client:\n    if: github.event.action == 'published'\n    runs-on: ubuntu-latest\n    needs: linux\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 24\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 10\n      # for OIDC-based publishing to npm\n      - name: setup npm v11\n        run: npm install -g npm@11\n      # this will override version 2.0.0 from package.json to the current git tag version\n      - run: npm version from-git --no-git-tag-version\n        working-directory: ui/api\n      - run: npm ci\n        working-directory: ui/api\n      - run: pnpm run build\n        working-directory: ui/api\n      - run: npm publish --provenance --access public\n        working-directory: ui/api\n"
  },
  {
    "path": ".gitignore",
    "content": "/tiles-profiles\n/*build*\n/.pkg.mutex\n/.clang-tidy\n/.idea\n.vscode/\n.DS_Store\n/deps\n*.zip\n*.bin\n*.pbf\n*.csv\n/osr\n/delfi\n/test/test_case_osr\n/fasta.json\n/adr.cista*\n/data\n/test/data*\ndist/\nfasta.json\nconfig.yaml\nconfig.yml\nconfig.ini\n\n"
  },
  {
    "path": ".pkg",
    "content": "[nigiri]\n  url=git@github.com:motis-project/nigiri.git\n  branch=master\n  commit=8b07193adf152f258ccfa207f922579d0a0e2195\n[cista]\n  url=git@github.com:felixguendling/cista.git\n  branch=master\n  commit=10abc43279bd99586d3433ea3b419727f46b8dd9\n[osr]\n  url=git@github.com:motis-project/osr.git\n  branch=master\n  commit=78ed3a45e5b543cf4a266df005faca5c2fad8ed2\n[utl]\n  url=git@github.com:motis-project/utl.git\n  branch=master\n  commit=d9930c90e440df761c8c4916c36072de9dd00f49\n[adr]\n  url=git@github.com:triptix-tech/adr.git\n  branch=master\n  commit=64ff88efc22622a79d38c7a7e101d4a86e906a1e\n[googletest]\n  url=git@github.com:motis-project/googletest.git\n  branch=master\n  commit=7b64fca6ea0833628d6f86255a81424365f7cc0c\n[net]\n  url=git@github.com:motis-project/net.git\n  branch=master\n  commit=bb00cafad46dd4ea4064b9651d982f31b9853c19\n[openapi-cpp]\n  url=git@github.com:triptix-tech/openapi-cpp.git\n  branch=master\n  commit=1e1e5bee6a3a73270595196b2aa7f41cbe7d9214\n[unordered_dense]\n  url=git@github.com:motis-project/unordered_dense.git\n  branch=main\n  commit=2c7230ae7f9c30849a5b089fb4a5d11896b45dcf\n[reflect-cpp]\n  url=git@github.com:motis-project/reflect-cpp.git\n  branch=main\n  commit=86fdcdd09a54b0f55de97110e1911d27f60e498a\n[tiles]\n  url=git@github.com:motis-project/tiles.git\n  branch=master\n  commit=6bd71d984eb699d7160f5c448bc14d5c57ddb4b7\n[boost]\n  url=git@github.com:motis-project/boost.git\n  branch=boost-1.89.0\n  commit=77467bf580d98ea06716e2931cbe3b1f28e0cd37\n[tg]\n  url=git@github.com:triptix-tech/tg.git\n  branch=main\n  commit=20c0f298b8ce58de29a790290f44dca7c4ecc364\n[mimalloc]\n  url=git@github.com:motis-project/mimalloc.git\n  branch=dev3\n  commit=b88ce9c8fd6b7c9208a43bcdb705de9f499dbad4\n[lz4]\n  url=git@github.com:motis-project/lz4.git\n  branch=dev\n  commit=ff69dbd1ad10852104257d5306874a07b76f0dbd\n[prometheus-cpp]\n  url=git@github.com:motis-project/prometheus-cpp.git\n  branch=master\n  commit=e420cd7cf3995a994220b40a36c987ac8e67c0bf\n[opentelemetry-cpp]\n  url=git@github.com:motis-project/opentelemetry-cpp.git\n  branch=main\n  commit=57a4c01aff876e08d9d37a3dec2c9899f0606909\n[ctx]\n  url=git@github.com:motis-project/ctx.git\n  branch=master\n  commit=9b495bdd798520007a4f1c13e51766a26f10ef10\n[geo]\n  url=git@github.com:motis-project/geo.git\n  branch=master\n  commit=4a410791d3a2d77eafae917e0607051bbd4fa659\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.20)\n\nproject(motis LANGUAGES C CXX ASM)\n\nset(CMAKE_POLICY_DEFAULT_CMP0077 NEW)\n\noption(MOTIS_MIMALLOC \"use mimalloc\" OFF)\n\nset(MOTIS_STACKTRACE \"AUTO\" CACHE STRING \"Enable stacktrace support (AUTO, ON, OFF)\")\nset_property(CACHE MOTIS_STACKTRACE PROPERTY STRINGS \"AUTO;ON;OFF\")\nset(_MOTIS_STACKTRACE_ON \"$<OR:$<STREQUAL:${MOTIS_STACKTRACE},ON>,$<AND:$<STREQUAL:${MOTIS_STACKTRACE},AUTO>,$<CONFIG:Debug>>>\")\n\nif (NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY)\n    if (MOTIS_MIMALLOC)\n        set(CMAKE_MSVC_RUNTIME_LIBRARY \"MultiThreaded$<$<CONFIG:Debug>:Debug>DLL\")\n        set(protobuf_MSVC_STATIC_RUNTIME OFF)\n    else ()\n        set(CMAKE_MSVC_RUNTIME_LIBRARY \"MultiThreaded$<$<CONFIG:Debug>:Debug>\")\n        set(protobuf_MSVC_STATIC_RUNTIME ON)\n    endif ()\nendif ()\n\nif (MOTIS_MIMALLOC)\n    set(CISTA_USE_MIMALLOC ON)\n    set(PPR_MIMALLOC ON)\n    set(ADR_MIMALLOC ON)\n    set(OSR_MIMALLOC ON)\n    set(TILES_MIMALLOC ON)\n    if(WIN32)\n        set(MI_BUILD_SHARED ON)\n    endif()\nendif()\n\ninclude(cmake/buildcache.cmake)\ninclude(cmake/pkg.cmake)\n\nif (MOTIS_MIMALLOC)\n    if(WIN32)\n        set(motis-mimalloc-lib mimalloc)\n        target_link_libraries(cista INTERFACE mimalloc)\n    else()\n        set(motis-mimalloc-lib mimalloc-obj)\n        target_link_libraries(cista INTERFACE mimalloc-static)\n    endif()\n    target_compile_definitions(cista INTERFACE CISTA_USE_MIMALLOC=1)\n    target_compile_definitions(boost INTERFACE BOOST_ASIO_DISABLE_STD_ALIGNED_ALLOC=1)\nendif()\n\n# --- LINT ---\noption(ICC_LINT \"Run clang-tidy with the compiler.\" OFF)\nif (ICC_LINT)\n    # clang-tidy will be run on all targets defined hereafter\n    include(cmake/clang-tidy.cmake)\nendif ()\n\nset(CMAKE_COMPILE_WARNING_AS_ERROR ON)\nif (\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"Clang\")\n    set(motis-compile-options\n            -Weverything\n            -Wno-c++98-compat\n            -Wno-c++98-compat-pedantic\n            -Wno-newline-eof\n            -Wno-missing-prototypes\n            -Wno-padded\n            -Wno-double-promotion\n            -Wno-undef\n            -Wno-undefined-reinterpret-cast\n            -Wno-float-conversion\n            -Wno-global-constructors\n            -Wno-exit-time-destructors\n            -Wno-switch-enum\n            -Wno-c99-designator\n            -Wno-zero-as-null-pointer-constant\n            -Wno-missing-noreturn\n            -Wno-undefined-func-template\n            -Wno-unsafe-buffer-usage\n            -Wno-c++20-compat\n            -Wno-reserved-macro-identifier\n            -Wno-documentation-unknown-command\n            -Wno-duplicate-enum\n            -Wno-ctad-maybe-unsupported\n            -Wno-unknown-pragmas\n            -Wno-c++20-extensions\n            -Wno-switch-default\n            -Wno-unused-template\n            -Wno-shadow-uncaptured-local\n            -Wno-documentation-deprecated-sync\n            -Wno-float-equal\n            -Wno-deprecated-declarations\n            -Wno-reserved-identifier\n            -Wno-implicit-int-float-conversion\n            -Wno-nrvo\n            -Wno-thread-safety-negative\n            -Wno-unused-private-field)\nelseif (\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"AppleClang\")\n    set(motis-compile-options -Wall -Wextra -Wno-unknown-pragmas -Wno-deprecated-declarations)\nelseif (MSVC)\n    set(motis-compile-options\n            /bigobj\n            # clang-21 libc++ seems not to have a special overloaded std::atomic<std::shared_ptr<T>>\n            # check if future version support std::atomic<std::shared_ptr<rt>>\n            /D_SILENCE_CXX20_OLD_SHARED_PTR_ATOMIC_SUPPORT_DEPRECATION_WARNING\n    )\nelse ()\n    set(motis-compile-options\n            -Wall\n            -Wextra\n            -Wno-array-bounds\n            -Wno-stringop-overread\n            -Wno-mismatched-new-delete\n            -Wno-maybe-uninitialized)\nendif ()\n\n\n# --- OPENAPI ---\nopenapi_generate(openapi.yaml motis-api motis::api)\n\n\n# --- LIB ---\nfile(GLOB_RECURSE motislib-files src/*.cc)\nadd_library(motislib ${motislib-files})\ntarget_include_directories(motislib PUBLIC include)\ntarget_compile_features(motislib PUBLIC cxx_std_23)\ntarget_compile_options(motislib PRIVATE ${motis-compile-options})\ntarget_link_libraries(motislib\n    nigiri\n    osr\n    adr\n    ctx\n    boost-json\n    motis-api\n    reflectcpp\n    web-server\n    tiles\n    pbf_sdf_fonts_res\n    ssl\n    crypto\n    tg\n    lz4_static\n    lb\n    web-server\n    prometheus-cpp::core\n    opentelemetry_trace\n    opentelemetry_exporter_otlp_http\n    lmdb\n    Boost::stacktrace\n    \"$<${_MOTIS_STACKTRACE_ON}:Boost::stacktrace_from_exception>\"\n)\n\n\n# --- EXE ---\nexecute_process(\n    COMMAND git describe --always --tags --dirty=-dirty\n    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}\n    OUTPUT_VARIABLE motis-git-tag\n    OUTPUT_STRIP_TRAILING_WHITESPACE\n)\nfile(GLOB_RECURSE motis-files exe/*.cc)\nadd_executable(motis ${motis-files})\ntarget_compile_features(motis PUBLIC cxx_std_23)\ntarget_compile_options(motis PRIVATE ${motis-compile-options})\nset_source_files_properties(exe/main.cc PROPERTIES COMPILE_DEFINITIONS MOTIS_VERSION=\"${motis-git-tag}\")\ntarget_link_libraries(motis\n    motislib\n    ianatzdb-res\n    pbf_sdf_fonts_res-res\n    tiles_server_res-res\n    address_formatting_res-res\n)\n\nif (\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"AppleClang\")\n    target_link_options(motis PRIVATE -Wl,-no_deduplicate)\nendif()\n\n# --- TEST ---\nadd_library(motis-generated INTERFACE)\ntarget_include_directories(motis-generated INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/generated)\nconfigure_file(\n        ${CMAKE_CURRENT_SOURCE_DIR}/test/test_dir.h.in\n        ${CMAKE_CURRENT_BINARY_DIR}/generated/test_dir.h\n)\nfile(GLOB_RECURSE motis-test-files test/*.cc)\nadd_executable(motis-test ${motis-test-files})\ntarget_link_libraries(motis-test motislib gmock web-server ianatzdb-res address_formatting_res-res motis-generated)\ntarget_compile_options(motis-test PRIVATE ${motis-compile-options})\n\n\n# --- TILES ---\nset_property(\n    TARGET motis tiles tiles-import-library\n    APPEND PROPERTY COMPILE_DEFINITIONS TILES_GLOBAL_PROGRESS_TRACKER=1)\nfile (CREATE_LINK ${CMAKE_SOURCE_DIR}/deps/tiles/profile ${CMAKE_BINARY_DIR}/tiles-profiles SYMBOLIC)\n\n# --- MIMALLOC ---\nif (MOTIS_MIMALLOC)\n    target_link_libraries(motis ${motis-mimalloc-lib})\n    target_compile_definitions(motis PUBLIC USE_MIMALLOC=1)\n    if(WIN32)\n        add_custom_command(\n                TARGET motis POST_BUILD\n                COMMAND \"${CMAKE_COMMAND}\" -E copy\n                $<TARGET_FILE:mimalloc>\n                $<TARGET_FILE_DIR:motis>\n                COMMENT \"Copy mimalloc.dll to output directory\"\n        )\n        add_custom_command(\n                TARGET motis POST_BUILD\n                COMMAND \"${CMAKE_COMMAND}\" -E copy\n                \"${CMAKE_SOURCE_DIR}/deps/mimalloc/bin/mimalloc-redirect.dll\"\n                $<TARGET_FILE_DIR:motis>\n                COMMENT \"Copy mimalloc-redirect.dll to output directory\"\n        )\n        add_custom_command(\n                TARGET motis POST_BUILD\n                COMMAND \"${CMAKE_SOURCE_DIR}/deps/mimalloc/bin/minject.exe\"\n                --force --inplace\n                $<$<CONFIG:Debug>:--postfix=debug>\n                $<TARGET_FILE:motis>\n                COMMENT \"Ensure mimalloc.dll is loaded first\"\n        )\n        add_custom_command(\n                TARGET motis-test POST_BUILD\n                COMMAND \"${CMAKE_SOURCE_DIR}/deps/mimalloc/bin/minject.exe\"\n                --force --inplace\n                $<$<CONFIG:Debug>:--postfix=debug>\n                $<TARGET_FILE:motis-test>\n                COMMENT \"Ensure mimalloc.dll is loaded first\"\n        )\n    endif()\n    if (MSVC)\n        target_link_options(motis PUBLIC \"/include:mi_version\")\n    endif ()\nendif()\n\n\n# --- UI ---\nadd_custom_target(motis-web-ui\n        COMMAND pnpm install && pnpm -r build\n        WORKING_DIRECTORY \"${CMAKE_CURRENT_SOURCE_DIR}/ui\"\n        VERBATIM\n)\nfile (CREATE_LINK ${CMAKE_SOURCE_DIR}/ui/build ${CMAKE_BINARY_DIR}/ui SYMBOLIC)\n\nforeach(t adr osr nigiri gtfsrt\n        geo tiles tiles-import-library\n        motis motis-api motislib)\n    target_compile_options(${t} PUBLIC ${MOTIS_TARGET_FLAGS})\nendforeach()\nif (MOTIS_MIMALLOC)\n    target_compile_options(mimalloc PUBLIC ${MOTIS_TARGET_FLAGS})\nendif()\n"
  },
  {
    "path": "CMakePresets.json",
    "content": "{\n  \"version\": 3,\n  \"cmakeMinimumRequired\": {\n    \"major\": 3,\n    \"minor\": 21,\n    \"patch\": 0\n  },\n  \"configurePresets\": [\n    {\n      \"name\": \"macos-x86_64\",\n      \"displayName\": \"MacOS x86_64 Release\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/macos-x86_64-release\",\n      \"cacheVariables\": {\n        \"BOOST_CONTEXT_ABI\": \"sysv\",\n        \"BOOST_CONTEXT_ARCHITECTURE\": \"x86_64\",\n        \"CMAKE_OSX_ARCHITECTURES\": \"x86_64\",\n        \"CMAKE_CXX_FLAGS\": \"-stdlib=libc++\",\n        \"CMAKE_BUILD_TYPE\": \"Release\"\n      }\n    },\n    {\n      \"name\": \"macos-arm64\",\n      \"displayName\": \"MacOS ARM64 Release\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/macos-arm64-release\",\n      \"cacheVariables\": {\n        \"CMAKE_OSX_ARCHITECTURES\": \"arm64\",\n        \"CMAKE_CXX_FLAGS\": \"-stdlib=libc++\",\n        \"BOOST_CONTEXT_ARCHITECTURE\": \"arm64\",\n        \"BOOST_CONTEXT_ABI\": \"aapcs\",\n        \"ENABLE_ASM\": \"OFF\",\n        \"CMAKE_BUILD_TYPE\": \"Release\"\n      }\n    },\n    {\n      \"name\": \"linux-amd64-release\",\n      \"displayName\": \"Linux AMD64 Release\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/amd64-release\",\n      \"toolchainFile\": \"/opt/x86_64-multilib-linux-musl/toolchain-amd64.cmake\",\n      \"cacheVariables\": {\n        \"CMAKE_EXE_LINKER_FLAGS\": \"-B/opt/mold\",\n        \"CMAKE_BUILD_TYPE\": \"Release\",\n        \"MOTIS_MIMALLOC\": \"ON\"\n      },\n      \"environment\": {\n        \"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\"\n      }\n    },\n    {\n      \"name\": \"linux-arm64-release\",\n      \"displayName\": \"Linux ARM64 Release\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/arm64-release\",\n      \"toolchainFile\": \"/opt/aarch64-unknown-linux-musl/toolchain-arm64.cmake\",\n      \"cacheVariables\": {\n        \"CMAKE_CROSSCOMPILING_EMULATOR\": \"qemu-aarch64-static\",\n        \"CMAKE_C_FLAGS\": \"-mcpu=neoverse-n1\",\n        \"CMAKE_CXX_FLAGS\": \"-mcpu=neoverse-n1\",\n        \"CMAKE_EXE_LINKER_FLAGS\": \"-B/opt/mold\",\n        \"CMAKE_BUILD_TYPE\": \"Release\",\n        \"MOTIS_MIMALLOC\": \"ON\"\n      },\n      \"environment\": {\n        \"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\"\n      }\n    },\n    {\n      \"name\": \"linux-sanitizer\",\n      \"displayName\": \"Linux Sanitizer\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/sanitizer\",\n      \"cacheVariables\": {\n        \"CMAKE_C_COMPILER\": \"clang-21\",\n        \"CMAKE_CXX_COMPILER\": \"clang++-21\",\n        \"CMAKE_EXE_LINKER_FLAGS\": \"-lc++abi\",\n        \"CMAKE_BUILD_TYPE\": \"Debug\",\n        \"CTX_ASAN\": \"ON\",\n        \"CMAKE_C_FLAGS\": \"-fsanitize=address,undefined -fsanitize-ignorelist=${sourceDir}/tools/ubsan-suppress.txt -fno-omit-frame-pointer\",\n        \"CMAKE_CXX_FLAGS\": \"-stdlib=libc++ -fsanitize=address,undefined -fno-omit-frame-pointer\"\n      },\n      \"environment\": {\n        \"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\"\n      }\n    },\n    {\n      \"name\": \"linux-debug\",\n      \"displayName\": \"Linux Debug\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/debug\",\n      \"cacheVariables\": {\n        \"CMAKE_BUILD_TYPE\": \"Debug\",\n        \"CMAKE_EXE_LINKER_FLAGS\": \"-B/opt/mold\",\n        \"CTX_VALGRIND\": \"ON\"\n      },\n      \"environment\": {\n        \"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\",\n        \"CXX\": \"/usr/bin/g++-13\",\n        \"CC\": \"/usr/bin/gcc-13\"\n      }\n    },\n    {\n      \"name\": \"linux-relwithdebinfo\",\n      \"displayName\": \"Linux RelWithDebInfo\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/relwithdebinfo\",\n      \"cacheVariables\": {\n        \"CMAKE_BUILD_TYPE\": \"RelWithDebInfo\",\n        \"CMAKE_EXE_LINKER_FLAGS\": \"-B/opt/mold\"\n      },\n      \"environment\": {\n        \"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\",\n        \"CXX\": \"/usr/bin/g++-13\",\n        \"CC\": \"/usr/bin/gcc-13\"\n      }\n    },\n    {\n      \"name\": \"clang-tidy\",\n      \"displayName\": \"Clang Tidy\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build/clang-tidy\",\n      \"cacheVariables\": {\n        \"CMAKE_C_COMPILER\": \"clang-21\",\n        \"CMAKE_CXX_COMPILER\": \"clang++-21\",\n        \"CMAKE_CXX_FLAGS\": \"-stdlib=libc++\",\n        \"CMAKE_EXE_LINKER_FLAGS\": \"-lc++abi\",\n        \"CMAKE_BUILD_TYPE\": \"Release\",\n        \"ICC_LINT\": \"On\"\n      },\n      \"environment\": {\n        \"BUILDCACHE_LUA_PATH\": \"/opt/buildcache/share/lua-examples\",\n        \"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\"\n      }\n    }\n  ],\n  \"buildPresets\": [\n    {\n      \"name\": \"macos-x86_64\",\n      \"configurePreset\": \"macos-x86_64\"\n    },\n    {\n      \"name\": \"macos-arm64\",\n      \"configurePreset\": \"macos-arm64\"\n    },\n    {\n      \"name\": \"linux-amd64-release\",\n      \"configurePreset\": \"linux-amd64-release\"\n    },\n    {\n      \"name\": \"linux-arm64-release\",\n      \"configurePreset\": \"linux-arm64-release\"\n    },\n    {\n      \"name\": \"clang-tidy\",\n      \"configurePreset\": \"clang-tidy\"\n    },\n    {\n      \"name\": \"linux-debug\",\n      \"configurePreset\": \"linux-debug\"\n    },\n    {\n      \"name\": \"linux-relwithdebinfo\",\n      \"configurePreset\": \"linux-relwithdebinfo\"\n    },\n    {\n      \"name\": \"linux-sanitizer\",\n      \"configurePreset\": \"linux-sanitizer\"\n    }\n  ]\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual\nidentity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall\n  community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or advances of\n  any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email address,\n  without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official email address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nfelix@triptix.tech.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of\nactions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or permanent\nban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the\ncommunity.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\nCommunity Impact Guidelines were inspired by\n[Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at\n[https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:3.20\nARG TARGETARCH\nADD motis-linux-$TARGETARCH/motis-linux-$TARGETARCH.tar.bz2 /\nRUN addgroup -S motis && adduser -S motis -G motis && \\\n    mkdir /data && \\\n    chown motis:motis /data\nEXPOSE 8080\nVOLUME [\"/data\"]\nUSER motis\nCMD [\"/motis\", \"server\", \"/data\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 MOTIS Project\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img src=\"logo.svg\" width=\"196\" height=\"196\"></p>\n\n> [!TIP]\n> :sparkles: Join the international MOTIS community at [**motis:matrix.org**](https://matrix.to/#/#motis:matrix.org)\n\nMOTIS stands for **M**odular **O**pen **T**ransportation **I**nformation **S**ystem.\nIt is an open-source software platform designed to facilitate\nefficient planning and routing in multi-modal transportation systems.\nDeveloped to handle *large-scale* transportation data,\nMOTIS integrates various modes of transport -\nsuch as walking, cycling, sharing mobility (e-scooters, bike sharing, car\nsharing), and public transport -\nto provide optimized routing solutions.\n\nMOTIS currently supports the following input formats:\n\n- (One) **OpenStreetMap `osm.pbf`** file for the street network, addresses, indoor-routing, etc. \n- (Multiple) **GTFS** (including GTFS Flex and GTFS Fares v2) feeds for static timetables\n- (Multiple) **GTFS-RT** feeds for real-time updates (delays, cancellations, track changes, service alerts)\n- (Multiple) **GBFS** feeds for sharing mobility\n\n*Working on (funded by [NLnet](https://nlnet.nl/project/MOTIS/))*: NeTEx and SIRI\n\nMOTIS provides an easy-to-use **REST API** (JSON via HTTP) with\nan [**OpenAPI specification**](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/motis-project/motis/refs/heads/master/openapi.yaml) ([source](openapi.yaml))\nthat 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/).\n\nAlso 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).\nPlease make sure to read the [Usage Policy](https://transitous.org/api/) before integrating this endpoint into your app.\n\n# Features\n\n> [!NOTE]  \n> :rocket: MOTIS is optimized for **high performance** with **low memory usage**.\n> \n> This enables _planet-sized_ deployments on affordable hardware.\n\nMOTIS is a swiss army knife for mobility and comes with all features you need for a next generation mobility platform:\n\n- **routing**: one mode walking, bike, car, sharing mobility / combined modes\n- **geocoding**: multi-language address and stop name completion with fuzzy string matching and resolution to geo coordinates\n- **reverse geocoding**: resolving geo coordinates to the closest address\n- **tile server**: background map tiles\n\nMOTIS uses efficient traffic day bitsets that allows efficient loading of **full year timetables**!\nLoading one year of timetable doesn't take much more RAM than loading one month.\n\nFeatures can be turned on and off as needed.\n\n# Quick Start\n\n- Create a folder with the following files.\n- Download MOTIS from\n  the [latest release](https://github.com/motis-project/motis/releases) and\n  extract the archive.\n- Download a OpenStreetMap dataset as `osm.pbf` (e.g.\n  from [Geofabrik](https://download.geofabrik.de/)) and place it in the folder\n- Download one or more GTFS datasets and place them in the folder \n\n```bash\n./motis config my.osm.pbf gtfs.zip  # generates a minimal config.yml\n./motis import                      # preprocesses data\n./motis server                      # starts a HTTP server on port 8080 \n```\n\nThis will preprocess the input files and create a `data` folder.\nAfter that, it will start a server.\n\n> [!IMPORTANT]\n> Ensure a valid timetable is used. If the timetable is outdated, it will not contain any trips to consider for upcoming dates.\n\nThis script will execute the steps described above for a small dataset for the city of Aachen, Germany:\n\n**Linux / macOS**\n\n```bash\n# set TARGET to linux-arm64, macos-arm64, ... to fit your setup\n# see release list for supported platforms\nTARGET=\"linux-amd64\"\nwget https://github.com/motis-project/motis/releases/latest/download/motis-${TARGET}.tar.bz2\ntar xf motis-${TARGET}.tar.bz2\nwget https://github.com/motis-project/test-data/raw/aachen/aachen.osm.pbf\nwget https://opendata.avv.de/current_GTFS/AVV_GTFS_Masten_mit_SPNV.zip\n./motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip\n./motis import\n./motis server\n```\n\n**Windows**\n\n```pwsh\nInvoke-WebRequest https://github.com/motis-project/motis/releases/latest/download/motis-windows.zip -OutFile motis-windows.zip\nExpand-Archive motis-windows.zip\nInvoke-WebRequest https://github.com/motis-project/test-data/archive/refs/heads/aachen.zip -OutFile aachen.zip\nExpand-Archive aachen.zip\n./motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip\n./motis import\n./motis server\n```\n\n# Documentation\n\n## Developer Setup\n\nBuild MOTIS from source:\n- [for Linux](docs/linux-dev-setup.md)\n- [for Windows](docs/windows-dev-setup.md)\n- [for macOS](docs/macos-dev-setup.md)\n\nSet up a server using your build:\n- [for Linux](docs/dev-setup-server.md)\n\nMOTIS uses [pkg](https://github.com/motis-project/pkg) for dependency management.\nSee its [README](https://github.com/motis-project/pkg/blob/master/README.md) for how to work with it.\n\n## Configuration\n\n- [Advanced Setups](docs/setup.md)\n"
  },
  {
    "path": "cmake/buildcache.cmake",
    "content": "option(NO_BUILDCACHE \"Disable build caching using buildcache\" Off)\n\n# PDB debug information is not supported by buildcache.\n# Store debug info in the object files.\nif (DEFINED ENV{GITHUB_ACTIONS})\n    string(REPLACE \"/Zi\" \"\" CMAKE_C_FLAGS_DEBUG \"${CMAKE_C_FLAGS_DEBUG}\")\n    string(REPLACE \"/Zi\" \"\" CMAKE_CXX_FLAGS_DEBUG \"${CMAKE_CXX_FLAGS_DEBUG}\")\n    string(REPLACE \"/Zi\" \"\" CMAKE_C_FLAGS_RELWITHDEBINFO \"${CMAKE_C_FLAGS_RELWITHDEBINFO}\")\n    string(REPLACE \"/Zi\" \"\" CMAKE_CXX_FLAGS_RELWITHDEBINFO \"${CMAKE_CXX_FLAGS_RELWITHDEBINFO}\")\nelse ()\n    string(REPLACE \"/Zi\" \"/Z7\" CMAKE_C_FLAGS_DEBUG \"${CMAKE_C_FLAGS_DEBUG}\")\n    string(REPLACE \"/Zi\" \"/Z7\" CMAKE_CXX_FLAGS_DEBUG \"${CMAKE_CXX_FLAGS_DEBUG}\")\n    string(REPLACE \"/Zi\" \"/Z7\" CMAKE_C_FLAGS_RELWITHDEBINFO \"${CMAKE_C_FLAGS_RELWITHDEBINFO}\")\n    string(REPLACE \"/Zi\" \"/Z7\" CMAKE_CXX_FLAGS_RELWITHDEBINFO \"${CMAKE_CXX_FLAGS_RELWITHDEBINFO}\")\nendif ()\n\nset(buildcache-bin ${CMAKE_CURRENT_BINARY_DIR}/buildcache/bin/buildcache)\nget_property(rule-launch-set GLOBAL PROPERTY RULE_LAUNCH_COMPILE SET)\nif (NO_BUILDCACHE)\n    message(STATUS \"NO_BUILDCACHE set, buildcache disabled\")\nelseif (rule-launch-set)\n    message(STATUS \"Global property RULE_LAUNCH_COMPILE already set - skipping buildcache\")\nelse ()\n    find_program(buildcache_program buildcache HINTS ${CMAKE_CURRENT_BINARY_DIR}/buildcache/bin)\n    if (buildcache_program)\n        message(STATUS \"Using buildcache: ${buildcache_program}\")\n        set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE \"${buildcache_program}\")\n    else ()\n        message(STATUS \"buildcache not found - downloading\")\n        if (APPLE)\n            set(buildcache-archive \"buildcache-macos.zip\")\n        elseif (UNIX AND ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL \"aarch64\")\n            set(buildcache-archive \"buildcache-linux-arm64.tar.gz\")\n        elseif (UNIX)\n            set(buildcache-archive \"buildcache-linux-amd64.tar.gz\")\n        elseif (WIN32)\n            set(buildcache-archive \"buildcache-windows.zip\")\n        else ()\n            message(FATAL \"Error: NO_BUILDCACHE was not set but buildcache was not in path and system OS detection failed\")\n        endif ()\n\n        set(buildcache-url \"https://gitlab.com/bits-n-bites/buildcache/-/releases/v0.31.7/downloads/${buildcache-archive}\")\n        message(STATUS \"Downloading buildcache binary from ${buildcache-url}\")\n        file(DOWNLOAD \"${buildcache-url}\" ${CMAKE_CURRENT_BINARY_DIR}/${buildcache-archive})\n        execute_process(\n                COMMAND ${CMAKE_COMMAND} -E tar xf ${CMAKE_CURRENT_BINARY_DIR}/${buildcache-archive}\n                WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})\n        message(STATUS \"using buildcache: ${buildcache-bin}\")\n        set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${buildcache-bin})\n    endif ()\nendif ()\n"
  },
  {
    "path": "cmake/clang-tidy.cmake",
    "content": "if (CMake_SOURCE_DIR STREQUAL CMake_BINARY_DIR)\n    message(FATAL_ERROR \"CMake_RUN_CLANG_TIDY requires an out-of-source build!\")\nendif ()\n\nfile(RELATIVE_PATH RELATIVE_SOURCE_DIR ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR})\n\nif (ICC_CLANG_TIDY_COMMAND)\n    set(CLANG_TIDY_COMMAND \"${ICC_CLANG_TIDY_COMMAND}\")\nelse ()\n    find_program(CLANG_TIDY_COMMAND NAMES clang-tidy-21)\nendif ()\n\nif (NOT CLANG_TIDY_COMMAND)\n    message(FATAL_ERROR \"CMake_RUN_CLANG_TIDY is ON but clang-tidy is not found!\")\nendif ()\n\nset(CMAKE_CXX_CLANG_TIDY \"${CLANG_TIDY_COMMAND}\")\n\nfile(SHA1 ${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy.in clang_tidy_sha1)\nset(CLANG_TIDY_DEFINITIONS \"CLANG_TIDY_SHA1=${clang_tidy_sha1}\")\nunset(clang_tidy_sha1)\n\nconfigure_file(.clang-tidy.in ${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy)"
  },
  {
    "path": "cmake/pkg.cmake",
    "content": "if (NOT DEFINED PROJECT_IS_TOP_LEVEL OR PROJECT_IS_TOP_LEVEL)\n    find_program(pkg-bin pkg HINTS /opt/pkg)\n    if (pkg-bin)\n        message(STATUS \"found pkg ${pkg-bin}\")\n    else ()\n        set(pkg-bin \"${CMAKE_BINARY_DIR}/dl/pkg\")\n        if (${CMAKE_SYSTEM_NAME} STREQUAL \"Linux\" AND ${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL \"aarch64\")\n            set(pkg-url \"pkg-linux-arm64\")\n        elseif (${CMAKE_SYSTEM_NAME} STREQUAL \"Linux\")\n            set(pkg-url \"pkg\")\n        elseif (${CMAKE_SYSTEM_NAME} STREQUAL \"Windows\")\n            set(pkg-url \"pkg.exe\")\n        elseif (${CMAKE_SYSTEM_NAME} STREQUAL \"Darwin\")\n            set(pkg-url \"pkgosx\")\n        else ()\n            message(STATUS \"Not downloading pkg tool. Using pkg from PATH.\")\n            set(pkg-bin \"pkg\")\n        endif ()\n\n        if (pkg-url)\n            if (NOT EXISTS ${pkg-bin})\n                message(STATUS \"Downloading pkg binary from https://github.com/motis-project/pkg/releases/latest/download/${pkg-url}\")\n                file(DOWNLOAD \"https://github.com/motis-project/pkg/releases/latest/download/${pkg-url}\" ${pkg-bin})\n                if (UNIX)\n                    execute_process(COMMAND chmod +x ${pkg-bin})\n                endif ()\n            else ()\n                message(STATUS \"Pkg binary located in project.\")\n            endif ()\n        endif ()\n    endif ()\n\n    if (DEFINED ENV{GITHUB_ACTIONS})\n        message(STATUS \"${pkg-bin} -l -h -f\")\n        execute_process(\n                COMMAND ${pkg-bin} -l -h -f\n                WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n                RESULT_VARIABLE pkg-result\n        )\n    else ()\n        message(STATUS \"${pkg-bin} -l\")\n        execute_process(\n                COMMAND ${pkg-bin} -l\n                WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n                RESULT_VARIABLE pkg-result\n        )\n    endif ()\n\n    if (NOT pkg-result EQUAL 0)\n        message(FATAL_ERROR \"pkg failed: ${pkg-result}\")\n    endif ()\n\n    if (IS_DIRECTORY \"${CMAKE_CURRENT_SOURCE_DIR}/deps\")\n        add_subdirectory(deps)\n    endif ()\n\n    set_property(\n            DIRECTORY\n            APPEND\n            PROPERTY CMAKE_CONFIGURE_DEPENDS\n            \"${CMAKE_CURRENT_SOURCE_DIR}/.pkg\"\n    )\nendif ()\n"
  },
  {
    "path": "docs/STYLE.md",
    "content": "# MOTIS C++ Style\n\n# Preamble\n\nBeware that these rules only apply to MOTIS C++ and are very opinionated.\nC++ has a big diversity of programming styles from \"C with classes\" to \"Modern C++\".\nA lot of codebases have specific rules that make sense in this specific context\n(e.g. embedded programming, gaming, Google search, etc.) and therefore different\nguidelines. Over the years we learned that the rules described here are a good fit\nfor this specific project.\n\nSo in general our goals are:\n\n- We want high-level, maintainable C++ code by default, not \"high level assembly\"\n- but: don’t use features just because you can (like template meta programming, etc.)\n\n# Style\n\n- Header names: **`*.h`**, Implementation names: **`*.cc`**\n- Don’t use include guards (`#ifndef #define #endif`), use **`#pragma once`**\n- Consistently use **`struct`** instead of `class`\n  - default visibility: public (which is what we need → no getter / setter)\n  - you don’t need to write a constructor for 1-line initialization\n- Always use ++i instead of i++ if it makes no difference for the program logic:\n  `for (auto i = 0U; i < 10; ++i) { … }`\n- Don't `using namespace std;`\n- Don’t use `NULL` or `0`, use **nullptr** instead\n- Don’t write `const auto&`, write **`auto const&`**\n- Don’t write `const char*`, write **`char const*`**\n\n\n# Case\n\n- Everything **`snake_case`** (as in the C++ Standard Library)\n- Template parameters **`PascalCase`** (as in the C++ Standard Library)\n- Constants **`kPascalCase`** (as in the Google C++ Styleguide), not `UPPER_CASE` to prevent collisions with macro names\n- Postfix **`member_variables_`** with an underscore to improve code readability when reading code without an IDE\n\n```cpp\nconstexpr auto kMyConstant = 3.141;\n\ntemplate <typename TemplateType, int Size>\nstruct my_class : public my_parent {\n  void member_fn(std::string const& fn_param) const override {\n    auto const local_cvar = abc();\n    auto local_var = def();\n  }\n  int my_field_;\n};\n```\n\n# Includes\n\n- Include only what you use (but everything you use!)\n- Group includes:\n  - for `.cc` files: first its own `.h` file\n  - Standard headers with `<...>` syntax\n    - C headers (use `<cstring>` instead of `<string.h>`, etc.)\n    - C++ standard library headers (e.g. `<string>`)\n  - Non-standard headers with `\"...\"` syntax\n    - generic to specific = boost libraries, then more and more specific\n    - last: project includes\n    - if available: local includes `\"./test_util.h\"` from the local folder (only done for tests)\n- Do not repeat include files from your own header file\n- Repeat everything else - even it's transitiveley included already through other headers.\n  The include might be removed from the header you include which leads broken compilation.\n  Try to make the compilation as robust as possible. \n\n\nExample include files for `message.cc`:\n```cpp\n#include \"motis/module/message.h\"\n\n#include <cstring>\n#include <string>\n\n#include \"boost/asio.hpp\"\n\n#include \"flatbuffers/idl.h\"\n#include \"flatbuffers/util.h\"\n\n#include \"motis/core/common/logging.h\"\n```\n\n# Simplify Code: Predicate Usage\n\n```cpp\n// bad\nif (is_valid()) {\n  set_param(false);\n} else {\n  set_param(true);\n}\n\n// bad\nset_param(is_valid() ? false : true);\n\n// good\nset_param(!is_valid());\n```\n\n# Always use Braces\n\n```cpp\n// bad\nfor (auto i = 0u; i < 10; ++i)\n  if (is_valid())\n    return get_a();\n  else\n    count_b();\n\n// good\nfor (auto i = 0u; i < 10; ++i) {\n  if (is_valid()) {\n    return get_a();\n  } else {\n    count_b();\n  }\n}\n```\n\n# Use Short Variable Names\n\nOnly use shortened version of the variable name if it's still obvious what the variable holds.\n\n- Index = `idx`\n- Input = `in`\n- Output = `out`\n- Request = `req`\n- Response = `res`\n- Initialization = `init`\n- ... etc.\n\nIf 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()`.\n\nDon't use `lhs` and `rhs` - for comparison with `friend bool operator==`. Use `a` and `b`.\n\n# Signatures in Headers\n\nOmit information that's not needed for a forward declaration.\n\n```cpp\nvoid* get_memory(my_memory_manager& memory_manager); // bad\n\nvoid* get_memory(my_memory_manager&); // good\n\n// const for value parameters is not needed in headers\nvoid calc_mask(bool const, bool const, bool const, bool const); // bad\n\nvoid calc_mask(bool local_traffic, // slightly less bad\n               bool long_distance_traffic,\n               bool local_stations,\n               bool long_distance_stations);\n\nvoid calc_mask(mask_options); // good\n```\n\n# Low Indentation\n\nTry to keep indentation at a minimum by handling cases one by one and bailing out early.\n\nExample:\n\nBad:\n\n```cpp\nint main(int argc, char** argv) {\n  if (argc > 1) {\n    for (int i = 0; i < argc; ++i) {\n      if (std::strcmp(\"hello\", argv[i]) == 0) {\n        /* ... 100 lines of code ... */\n      }\n    }\n  }\n}\n```\n\nGood:\n\n```cpp\nint main(int argc, char** argv) {\n  if (argc <= 1) {\n    return 0;\n  }\n  for (int i = 0; i < argc; ++i) {\n    if (std::strcmp(\"hello\", argv[i]) != 0) {\n      continue;\n    }\n    /* ... 100 lines of code ... */\n  }\n}\n```\n\n# Function Length / File Length\n\nFunctions 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.\n\n- Better: split earlier if it makes sense! Files are free! (more than one responsibility)\n- Split later i.e. if you want to keep one block of logic without interruption (easier to understand)\n\n# Pointers\n\nRead C++ data types from right to left:\n\n**`int const* const`** \n- `const` (read only) pointer (address can't be modified)\n- to `const int` (int value at address can't be modified)\n\n**int const&**\n- reference\n- on a const `int` value (read only)\n\n**auto const&**\n- reference\n- on a value (type deduced by the compiler)\n\n# Use RAII\n\nWhenever possible use RAII to manage resource like memory (`std::unique_ptr`, `std::shared_ptr`, etc.), files (`std::fstream`), network sockets (Boost Asio), etc.\n\nThis means we do not want `new` or `delete` - except for placement new or placement delete in some very specific cases.\n\n# Use `utl` Library\n\nIf 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.\n\n# Use `strong` types\n\nUse `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.\n\n# `const`\n\nMake 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).\n\n# Initialization\n\nUse [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.\n\n# Namespaces\n\nRename long namespace names instead of importing them completely.\n\n```cpp\nusing boost::program_options;  // bad\nnamespace po = boost::program_options; // good\n```\n\nThis way we still know where functions come from when reading code.\nIt becomes hard to know where a function came from when several large namespaces are completely imported.\n\nDon't alias or import namespaces in header files.\n\n# AAA-Style\n\nUse [Almost Always Auto (AAA)](https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/) style if possible.\n\n- Program against interfaces\n- Abstraction\n- Less typing\n\nExample: `for (auto const& el : c())`\n\nNo client code change if\n- c returns another collection type (i.e. set instead of vector)\n- the element type changes but still has a compatible interface\n\n# No Raw Loops\n\nIt takes time to understand a raw for loop:\n```cpp\nfor (int i = -1; i <= 9; i += 2) {\n  if (i % 2 == 0) { continue; }\n  if (i > 5 && i % 2 == 1) { break; }\n  printf(\"%d\\n\", i/3);\n}\n```\n\n- Raw for loops can\n  - do crazy things\n  - be boring (can often be expressed with a standard library algorithm!!)\n  \n- Find an element loop → `std::find`, `std::lower_bound`, ...\n- Check each element loop → `std::all_of`, `std::none_of`, `std::any_of`\n- Conversion loop → `std::transform`, `utl::to_vec`\n- Counting: `std::count_if`, `std::accumulate`\n- Sorting: `std::sort`, `std::nth_element`, `std::is_sorted`\n- Logic: `std::all_of`, `std::any_of`\n- Iterating multiple elements at once: `utl::zip`, `utl::pairwise`, `utl::nwise`\n- Erasing elements: `utl::erase_if`, `utl::erase_duplicates`\n- etc.\n\nHint: `utl` provides a cleaner interface wrapping `std::` functions for collections so you don't have to call `begin` and `end` all the time!\n\nBenefits:\n- Function name tells the reader of your code already what it does!\n- Standard library implementation does not contain errors and is performant!\n\nAlternative (if no function in the standard or `utl` helps):\n- Use range based for loop if there's no named function: `for (auto const& el : collection) { .. }`\n\n# Comparators\n\nEither use\n- Preferred: mark the operator you need `= default;`\n- If that doesn't do the job you can check `CISTA_FRIEND_COMPARABLE`\n- If you want to be selective and only compare a subset of member variables: `std::tie(a_, b_) == std::tie(a_, b_)`\n\n# Set/Map vs Vector\n\nOur go-to data structure is `std::vector`. (Hash-)maps and (hash-)sets are very expensive.\n\nNever use `std::unordered_map`. We have better alternatives in all projects (e.g. unordered_dense).\n\n## `vecvec` and `vector_map`\n\n- Use `vector_map` for mappings with a `strong` key type and a continuous domain.\n- Prefer using `vecvec<T>` instead of `vector<vector<T>>`, as data is stored and accessed more efficient. To store data, that may appear in any order, you may consider `paged_vecvec` instead.\n\n# Tooling\n\n- 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`.\n    - **Notice**: Some checks can cause false positive and should be disabled if necessary (compare `ci.yml`).  \n      Example: `ASAN_OPTIONS=alloc_dealloc_mismatch=0`\n- Check your code with `valgrind`.\n\n# Spirit\n\n- No deep inheritance hierarchies (no \"enterprise\" code)\n- Don't write getters / setters for member variables: just make them public\n  (which is the default for `struct` - remember: always use structs)\n- 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).\n- No GoF \"design patterns\" (Factory, Visitor, ...) if there is a simpler solution (there's always a simpler solution)\n- Function / struct length:\n  - it should be possible to understand every function by shortly looking at it\n  - hints where to split:\n    - single responsibility\n    - short enough to be reusable in another context\n- Don’t write “extensible” code that cares for functionality you might need at some point in the future. Just solve the problem at hand.\n- Build the **smallest and simplest** solution possible that solves your problem\n- Use abstractions to avoid thinking about details: helps to keep functions short\n- Comment only the tricky / hacky pieces of your code\n  (there should not be too many comments, otherwise your code is bad)\n- Instead of comments use good (but short!) names for variables and functions\n- Less code = less maintenance, less places for bugs, easier to understand\n- Write robust code: `utl::verify()` assumptions about input data\n"
  },
  {
    "path": "docs/dev-setup-server.md",
    "content": "# Setting up a server from a development build\n\n1. Build `motis`. Refer to the respective documentation if necessary:\n   - [for Linux](linux-dev-setup.md)\n   - [for Windows](windows-dev-setup.md)\n   - [for macOS](macos-dev-setup.md)\n\n\n2. Build the UI:\n    ```shell\n    motis$ cd ui\n    motis/ui$ pnpm install\n    motis/ui$ pnpm -r build\n    ```\n   \n3. Move the UI build into the build folder of `motis`:\n    ```shell\n    motis$ mv ui/build build/ui\n    ```\n   \n4. Copy the tiles profiles to the `motis` build folder:\n    ```shell\n    motis$ cp -r deps/tiles/profile build/tiles-profiles\n    ```\n   \n5. Download OpenStreetMap and timetable datasets and place them in the build folder of `motis`:\n    ```shell\n    motis/build$ wget https://github.com/motis-project/test-data/raw/aachen/aachen.osm.pbf\n    motis/build$ wget https://opendata.avv.de/current_GTFS/AVV_GTFS_Masten_mit_SPNV.zip\n    ```\n   \n6. Run `motis config` on the downloaded datasets to create a config file:\n    ```shell\n    motis/build$ ./motis config aachen.osm.pbf AVV_GTFS_Masten_mit_SPNV.zip\n    ```\n\n7. Run `motis import` and then start the server using `motis server`:\n    ```shell\n    motis/build$ ./motis import\n    motis/build$ ./motis server\n    ```\n\n8. Open `localhost:8080` in a browser to see if everything is working.\n"
  },
  {
    "path": "docs/elevation-setup.md",
    "content": "# Setting up elevation tiles\n\nThis page explains how to set up elevation tiles, that are required for elevation profiles.\n\nFor performance reasons, all tile data must be stored uncompressed.\nThis will require roughly `350 GB` for the full SRTMGL1 data set.\n\nAfter the import, the elevation data requires less disk space than the\n`way_osm_nodes_index.bin` and `way_osm_nodes_data.bin` files.\n\n## Data formats\n\nElevation data must be provided in a tile data format, which has been implemented in and is supported by `osr`.\n\nCurrently supported formats:\n- **SRTMHGT**: Format used for SRTMGL1 data\n- **EHdr**, also known _BIL format_: Format, that has been used for SRTM data in the past\n\nFor more details about these formats and a list of additional raster drivers also see https://gdal.org/en/stable/drivers/raster/\n\n## Set up SRTM files\n\nThe SRTMGL1 data is provided by the LP DAAC at https://lpdaac.usgs.gov/products/srtmgl1v003/\n\nNotice that the website will be moving by June 17, 2025.\nThe data should then be available at the new website: https://www.earthdata.nasa.gov/centers/lp-daac\n\n### Data download\n\nAll HGT data tiles can be downloaded using _NASA Earthdata Search_: https://search.earthdata.nasa.gov/search?q=C2763266360-LPCLOUD\nDownloading tiles requires a free account.\n\n1. Log in and create an account if necessary\n1. 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\n1. Select all tiles you want to download\n    - Use can use the _Search by spatial polygon_ or _Search by spatial rectangle_ options in the bottom right to select smaller regions\n1. Press _Download All_, once the tiles have been selected and continue by confirming with _Download Data_\n1. Files can now be downloaded directly (_Download Files_) or using the _Download Script_.  \n    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.\n1. 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`\n\n**Important**: HGT tiles not matching the naming convention will not be found\n\nOn Unix based systems the following script can be used to extract all ZIP files into a new subdirectory `hgt`:\n\n```sh\n#!/bin/sh\n[ -d 'hgt' ] || mkdir 'hgt'  # Create directory if necessary\nfor f in *.hgt.zip; do\n  [ -s \"${f}\" ] || continue  # Ensure file exists and is not empty\n  unzip `# Always override` -o \"${f}\" `# Extract into directory` -d 'hgt'\n  rm -f \"${f}\"  # Delete compressed file\ndone\n```\n\n### Using the SRTM elevation data\n\nAssume the extracted HGT tiles are stored at `<path>/srtm`.\n\n1. Edit `config.yml`:\n    ```yml\n    street_routing:\n      elevation_data_dir: <path>/srtm\n    ```\n    This replaces any existing setting `street_routing: true|false`\n1. Run `motis import` to import the elevation data\n\n## Using multiple data tile formats\n\nFor global routing using elevation data, multiple data sources might be required.\nIt's therefore possible to use tiles with different formats simultaneously.\n\n1. Create a new directory `<elevation-dir>`\n1. For each data source create a new sub directory\n1. Move all elevation data files into the corresponding directories\n1. Set `elevation_data_dir: <elevation-dir>` in `config.yml`\n\nEnsure the directory only contains the elevation data, as adding, removing and renaming files will trigger a new import.\n\nAs all sub directories will be searched, it's also possible to split a data set into multiple directories if desired.\n"
  },
  {
    "path": "docs/linux-dev-setup.md",
    "content": "> [!NOTE]  \n> Due to developer capacity constraints we cannot support newer or older compilers.\n> We also cannot support other versions of dependencies.\n> 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).\n> To exactly reproduce the CI build, you need to use the according preset from our [CMakePresets.json](../CMakePresets.json).\n\nRequirements:\n\n- A recent C++ compiler: Either [Clang](https://llvm.org/) 21 or GCC 13\n- CMake 3.17 (or newer): [cmake.org](https://cmake.org/download/) ([Ubuntu APT Repository](https://apt.kitware.com/))\n- Ninja: [ninja-build.org](https://ninja-build.org/)\n- Git\n\n> [!CAUTION]\n> Unix Makefiles are not working. Please use Ninja to build.\n\n> [!CAUTION]\n> Motis' dependency management `pkg` requires that the project is cloned via SSH using an SSH key without a passphrase.\n> See:\n> - [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)\n> - [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)\n\n> [!NOTE]\n> Using a different compiler version is not officially supported but might work nevertheless when passing\n> `--compile-no-warning-as-error` to CMake.\n\n## Build with GCC\n\n```sh\ngit clone git@github.com:motis-project/motis.git\ncd motis\nmkdir build && cd build\nCXX=g++-13 CC=gcc-13 cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -GNinja ..\nninja\n```\n\n\n## Build with Clang\n\n```sh\ngit clone git@github.com:motis-project/motis.git\ncd motis\nmkdir build && cd build\nCXX=clang++-21 CC=clang-21 CXXFLAGS=-stdlib=libc++ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -GNinja ..\nninja\n```\n"
  },
  {
    "path": "docs/macos-dev-setup.md",
    "content": "Requirements:\n\n- macOS 10.15 or newer\n- Command Line Tools for Xcode or Xcode: `xcode-select --install` or [manual download](https://developer.apple.com/downloads)\n- [CMake](https://cmake.org/download/) 3.17 (or newer)\n- [Ninja](https://ninja-build.org/)\n- Git\n\n(Git, Ninja, and CMake can be installed via HomeBrew)\n\n> [!CAUTION]\n> Unix Makefiles are not working. Please use Ninja to build.\n\n> [!CAUTION]\n> Motis' dependency management `pkg` requires that the project is cloned via SSH using an SSH key without a passphrase.\n> See:\n> - [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)\n> - [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)\n\nTo build `motis`:\n\n```sh\ngit clone git@github.com:motis-project/motis.git\ncd motis\nmkdir build && cd build\ncmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -GNinja ..\nninja\n```"
  },
  {
    "path": "docs/python-client.md",
    "content": "# Python client for MOTIS\n\n## Install dependencies\n```sh\npip install openapi-python-client\n```\n\n## Generate Python code from OpenAPI specifications\n```sh\nopenapi-python-client generate --path openapi.yaml --output-path motis_api_client --meta none\n```\n\n## Use code (example)\n```python\nfrom motis_api_client import Client\nfrom motis_api_client.api.routing import one_to_all\n\nwith Client(base_url='http://localhost:8080') as client:\n  res = one_to_all.sync(one='52.520806, 13.409420', max_travel_time=30, client=client)\n\nres\n```\n"
  },
  {
    "path": "docs/scripting.md",
    "content": "# User Scripts\n\nMOTIS 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:\n\nIf no script is defined or a processing function is not given for a type, the default behaviour will be applied.\n\n\n## Configuration\n\nScripts 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.\n\nExample configuration with script property set:\n\n```\ntimetable:\n  datasets:\n    nl:\n      path: nl_ovapi.gtfs.zip\n      rt:\n        - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb\n        - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb\n      script: my-script.lua\n```\n\n## Types\n\n### Translation List\n\nSome string fields are translated. Their default getter (e.g. `get_name`) now\nreturns the default string, while the accompanying `get_*_translations`\nfunctions expose the full translation list. Lists can be accessed with\n[sol2 container operations](https://sol2.readthedocs.io/en/latest/containers.html).\nEach entry in that list is of type\n`translation` and provides:\n\n  - `get_language`\n  - `set_language`\n  - `get_text`\n  - `set_text`\n\nExample snippet of how to read and write translations:\n\n```lua\nfunction process_route(route)\n  route:set_short_name({\n    translation.new('en', 'EN_SHORT_NAME'),\n    translation.new('de', 'DE_SHORT_NAME'),\n    translation.new('fr', 'FR_SHORT_NAME')\n  })\n  route:get_short_name_translations():add(translation.new('hu', 'HU_SHORT_NAME'))\n  print(route:get_short_name_translations():get(1):get_text())\n  print(route:get_short_name_translations():get(1):get_language())\nend\n```\n\n### Location (stops, platforms, tracks)\n\nprocessing via `function process_location()`\n\n  - `get_id`\n  - `get_name`\n  - `get_name_translations`\n  - `set_name`\n  - `get_platform_code`\n  - `get_platform_code_translations`\n  - `set_platform_code`\n  - `get_description`\n  - `get_description_translations`\n  - `set_description`\n  - `get_pos`\n  - `set_pos`\n  - `get_timezone`\n  - `set_timezone`\n  - `get_transfer_time`\n  - `set_transfer_time` \n\n### Agency (as defined in GTFS `agencies.txt`)\n\nprocessing via `function process_agency(agency)`\n\n  - `get_id`\n  - `get_name`\n  - `get_name_translations`\n  - `set_name`\n  - `get_url`\n  - `get_url_translations`\n  - `set_url`\n  - `get_timezone`\n  - `set_timezone`\n\n### Routes (as defined in GTFS `routes.txt`)\n\nprocessing via `function process_route(location)`\n\n  - `get_id`\n  - `get_short_name`\n  - `get_short_name_translations`\n  - `set_short_name`\n  - `get_long_name`\n  - `get_long_name_translations`\n  - `set_long_name`\n  - `get_route_type`\n  - `set_route_type`\n  - `get_color`\n  - `set_color`\n  - `get_clasz`  (deprecated, use `get_route_type`)\n  - `set_clasz`  (deprecated, use `set_route_type`)\n  - `get_text_color`\n  - `set_text_color`\n  - `get_agency`\n\nThe following constants can be used for `set_route_type`:\n\n- `GTFS_TRAM`\n- `GTFS_SUBWAY`\n- `GTFS_RAIL`\n- `GTFS_BUS`\n- `GTFS_FERRY`\n- `GTFS_CABLE_TRAM`\n- `GTFS_AERIAL_LIFT`\n- `GTFS_FUNICULAR`\n- `GTFS_TROLLEYBUS`\n- `GTFS_MONORAIL`\n- `RAILWAY_SERVICE`\n- `HIGH_SPEED_RAIL_SERVICE`\n- `LONG_DISTANCE_TRAINS_SERVICE`\n- `INTER_REGIONAL_RAIL_SERVICE`\n- `CAR_TRANSPORT_RAIL_SERVICE`\n- `SLEEPER_RAIL_SERVICE`\n- `REGIONAL_RAIL_SERVICE`\n- `TOURIST_RAILWAY_SERVICE`\n- `RAIL_SHUTTLE_WITHIN_COMPLEX_SERVICE`\n- `SUBURBAN_RAILWAY_SERVICE`\n- `REPLACEMENT_RAIL_SERVICE`\n- `SPECIAL_RAIL_SERVICE`\n- `LORRY_TRANSPORT_RAIL_SERVICE`\n- `ALL_RAILS_SERVICE`\n- `CROSS_COUNTRY_RAIL_SERVICE`\n- `VEHICLE_TRANSPORT_RAIL_SERVICE`\n- `RACK_AND_PINION_RAILWAY_SERVICE`\n- `ADDITIONAL_RAIL_SERVICE`\n- `COACH_SERVICE`\n- `INTERNATIONAL_COACH_SERVICE`\n- `NATIONAL_COACH_SERVICE`\n- `SHUTTLE_COACH_SERVICE`\n- `REGIONAL_COACH_SERVICE`\n- `SPECIAL_COACH_SERVICE`\n- `SIGHTSEEING_COACH_SERVICE`\n- `TOURIST_COACH_SERVICE`\n- `COMMUTER_COACH_SERVICE`\n- `ALL_COACHS_SERVICE`\n- `URBAN_RAILWAY_SERVICE`\n- `METRO_SERVICE`\n- `UNDERGROUND_SERVICE`\n- `URBAN_RAILWAY_2_SERVICE`\n- `ALL_URBAN_RAILWAYS_SERVICE`\n- `MONORAIL_SERVICE`\n- `BUS_SERVICE`\n- `REGIONAL_BUS_SERVICE`\n- `EXPRESS_BUS_SERVICE`\n- `STOPPING_BUS_SERVICE`\n- `LOCAL_BUS_SERVICE`\n- `NIGHT_BUS_SERVICE`\n- `POST_BUS_SERVICE`\n- `SPECIAL_NEEDS_BUS_SERVICE`\n- `MOBILITY_BUS_SERVICE`\n- `MOBILITY_BUS_FOR_REGISTERED_DISABLED_SERVICE`\n- `SIGHTSEEING_BUS_SERVICE`\n- `SHUTTLE_BUS_SERVICE`\n- `SCHOOL_BUS_SERVICE`\n- `SCHOOL_AND_PUBLIC_BUS_SERVICE`\n- `RAIL_REPLACEMENT_BUS_SERVICE`\n- `DEMAND_AND_RESPONSE_BUS_SERVICE`\n- `ALL_BUSS_SERVICE`\n- `TROLLEYBUS_SERVICE`\n- `TRAM_SERVICE`\n- `CITY_TRAM_SERVICE`\n- `LOCAL_TRAM_SERVICE`\n- `REGIONAL_TRAM_SERVICE`\n- `SIGHTSEEING_TRAM_SERVICE`\n- `SHUTTLE_TRAM_SERVICE`\n- `ALL_TRAMS_SERVICE`\n- `WATER_TRANSPORT_SERVICE`\n- `AIR_SERVICE`\n- `FERRY_SERVICE`\n- `AERIAL_LIFT_SERVICE`\n- `TELECABIN_SERVICE`\n- `CABLE_CAR_SERVICE`\n- `ELEVATOR_SERVICE`\n- `CHAIR_LIFT_SERVICE`\n- `DRAG_LIFT_SERVICE`\n- `SMALL_TELECABIN_SERVICE`\n- `ALL_TELECABINS_SERVICE`\n- `FUNICULAR_SERVICE`\n- `TAXI_SERVICE`\n- `COMMUNAL_TAXI_SERVICE`\n- `WATER_TAXI_SERVICE`\n- `RAIL_TAXI_SERVICE`\n- `BIKE_TAXI_SERVICE`\n- `LICENSED_TAXI_SERVICE`\n- `PRIVATE_HIRE_VEHICLE_SERVICE`\n- `ALL_TAXIS_SERVICE`\n- `MISCELLANEOUS_SERVICE`\n- `HORSE_DRAWN_CARRIAGE_SERVICE`\n\nThe color is currently set as unsigned 32bit integer. In future versions, we might change this to a hex string like `#FF0000`.\n\n### Trips (as defined in GTFS `trips.txt`)\n  \nprocessing via `function process_trip(trip)`\n\n  - `get_id`\n  - `get_headsign`\n  - `get_headsign_translations`\n  - `set_headsign`\n  - `get_short_name`\n  - `get_short_name_translations`\n  - `set_short_name`\n  - `get_display_name`\n  - `get_display_name_translations`\n  - `set_display_name`\n  - `get_route`\n\n### Geo Location\n\nThis type is used for stop coordinates in `process_location()` for `location:get_pos()` and `location:set_pos`.\n\n  - `get_lat`\n  - `get_lng`\n  - `set_lat`\n  - `set_lng`\n\n\n## Filtering\n\nEach processing function can return a boolean which will be interpreted as\n\n  - `true`: keep this entity\n  - `false`: don't keep this entity\n\nIf 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`).\n\nFiltering has the following effects:\n\n  - In case an agency is removed, all its routes and trips will be removed as well\n  - In case a route is removed, all its trips will be removed as well\n  - If locations are filtered, the locations will not be removed from trips and transfers referencing those stops\n\n\n## Out of Scope\n\nScripting 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`). \n\n\n## Example\n\nThis 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.\n\n```lua\nfunction process_location(stop)\n  local name = stop:get_name()\n  if string.sub(name, -7) == ' Berlin' then\n    stop:set_name(string.sub(name, 1, -8))\n  end\n\n  local pos = stop:get_pos()\n  pos:set_lat(stop:get_pos():get_lat() + 2.0)\n  pos:set_lng(stop:get_pos():get_lng() - 2.0)\n  stop:set_pos(pos)\n\n  stop:set_description(stop:get_description() .. ' ' .. stop:get_id() .. ' YEAH')\n  stop:set_timezone('Europe/Berlin')\n  stop:set_transfer_time(stop:get_transfer_time() + 98)\n  stop:set_platform_code(stop:get_platform_code() .. 'A')\n\n  return true\nend\n\nfunction process_route(route)\n  if route:get_id() == 'R_RE4' then\n    return false\n  end\n\n  if route:get_route_type() == 3 then\n    route:set_clasz(7)\n    route:set_route_type(101)\n  elseif route:get_route_type() == 1 then\n    route:set_clasz(8)\n    route:set_route_type(400)\n  end\n\n  if route:get_agency():get_name() == 'Deutsche Bahn' and route:get_route_type() == 101 then\n    route:set_short_name('RE ' .. route:get_short_name())\n  end\n\n  return true\nend\n\nfunction process_agency(agency)\n  if agency:get_id() == 'TT' then\n    return false\n  end\n\n  if agency:get_name() == 'Deutsche Bahn' and agency:get_id() == 'DB' then\n    agency:set_url(agency:get_timezone())\n    agency:set_timezone('Europe/Berlin')\n    agency:set_name('SNCF')\n    return true\n  end\n  return false\nend\n\nfunction process_trip(trip)\n  if trip:get_route():get_route_type() == 101 then\n    -- Prepend category and eliminate leading zeros (e.g. '00123' -> 'ICE 123')\n    trip:set_short_name('ICE ' .. string.format(\"%d\", tonumber(trip:get_short_name())))\n    trip:set_display_name(trip:get_short_name())\n  end\n  return trip:get_id() == 'T_RE1'\nend\n```\n\n\n## Future Work\n\nThere 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.\n\nAnother 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.\n"
  },
  {
    "path": "docs/setup.md",
    "content": "# Advanced Configuration\n\nThis 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.\n\n```yaml\nserver:\n  port: 8080\n  web_folder: ui\nosm: netherlands-latest.osm.pbf\ntimetable:\n  datasets:\n    nl:\n      path: nl_ovapi.gtfs.zip\n      rt:\n        - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb\n        - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb\n    ch:\n      path: ch_opentransportdataswiss.gtfs.zip\n      rt:\n        - url: https://api.opentransportdata.swiss/gtfsrt2020\n          headers:\n            Authorization: MY_API_KEY\n          protocol: gtfsrt\ngbfs:\n  feeds:\n    montreal:\n      url: https://gbfs.velobixi.com/gbfs/gbfs.json\n    # Example feed for header usage\n    example-feed:\n      url: https://example.org/gbfs\n      headers:\n        authorization: MY_OTHER_API_KEY\n        other-header: other-value\ntiles:\n  profile: tiles-profiles/full.lua\nstreet_routing:\n  elevation_data_dir: srtm/\ngeocoding: true\nosr_footpath: true\n```\n\nThis expands to the following configuration:\n\n```yaml\nserver:\n  host: 0.0.0.0                     # host (default = 0.0.0.0)\n  port: 8080                        # port (default = 8080)\n  web_folder: ui                    # folder with static files to serve\n  n_threads: 24                     # default (if not set): number of hardware threads\n  data_attribution_link: https://creativecommons.org/licenses/by/4.0/ # link to data sources or license exposed in HTTP headers and UI\nosm: netherlands-latest.osm.pbf     # required by tiles, street routing, geocoding and reverse-geocoding\ntiles:                              # tiles won't be available if this key is missing\n  profile: tiles-profiles/full.lua  # currently `background.lua` (less details) and `full.lua` (more details) are available\n  db_size: 1099511627776            # default size for the tiles database (influences VIRT memory usage)\n  flush_threshold: 10000000         # usually don't change this (less = reduced memory usage during tiles import)\ntimetable:                          # if not set, no timetable will be loaded\n  first_day: TODAY                  # first day of timetable to load, format: \"YYYY-MM-DD\" (special value \"TODAY\")\n  num_days: 365                     # number of days to load, default is 365 days\n  railviz: true                     # enable viewing vehicles in real-time on the map, requires some extra lookup data structures\n  with_shapes: true                 # extract and serve shapes (if disabled, direct lines are used)\n  adjust_footpaths: true            # if footpaths are too fast, they are adjusted if set to true\n  merge_dupes_intra_src: false      # duplicates within the same datasets will be merged\n  merge_dupes_inter_src: false      # duplicates withing different datasets will be merged\n  link_stop_distance: 100           # stops will be linked by footpaths if they're less than X meters (default=100m) apart\n  update_interval: 60               # real-time updates are polled every `update_interval` seconds\n  http_timeout: 30                  # maximum time in seconds the real-time feed download may take\n  incremental_rt_update: false      # false = real-time updates are applied to a clean slate, true = no data will be dropped\n  max_footpath_length: 15           # maximum footpath length when transitively connecting stops or for routing footpaths if `osr_footpath` is set to true\n  max_matching_distance: 25.0       # maximum distance from geolocation to next OSM ways that will be found\n  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)\n  datasets:                         # map of tag -> dataset\n    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\n      path: ch_opentransportdataswiss.gtfs.zip\n      default_bikes_allowed: false\n      rt:\n        - url: https://api.opentransportdata.swiss/gtfsrt2020\n          headers:\n            Authorization: MY_API_KEY\n          protocol: gtfsrt          # specify the real time protocol (default: gtfsrt)\n    nl:\n      path: nl_ovapi.gtfs.zip\n      default_bikes_allowed: false\n      rt:\n        - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb\n        - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb\n      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`\ngbfs:\n  feeds:\n    montreal:\n      url: https://gbfs.velobixi.com/gbfs/gbfs.json\n    example-feed:\n      url: https://example.org/gbfs\n      headers:\n        authorization: MY_OTHER_API_KEY\n        other-header: other-value\nstreet_routing:                   # enable street routing (default = false; Using boolean values true/false is supported for backward compatibility)\n  elevation_data_dir: srtm/       # folder which contains elevation data, e.g. SRTMGL1 data tiles in HGT format\nlimits:\n  stoptimes_max_results: 256      # maximum number of stoptimes results that can be requested\n  plan_max_results: 256           # maximum number of plan results that can be requested via numItineraries parameter\n  plan_max_search_window_minutes: 5760 # maximum (minutes) for searchWindow parameter (seconds), highest possible value: 21600 (15 days)\n  onetomany_max_many: 128         # maximum accepted number of many locations for one-to-many requests\n  onetoall_max_results: 65535     # maximum number of one-to-all results that can be requested\n  onetoall_max_travel_minutes: 90 # maximum travel duration for one-to-all query that can be requested\n  routing_max_timeout_seconds: 90 # maximum duration a routing query may take\n  gtfsrt_expose_max_trip_updates: 100 # how many trip updates are allowed to be exposed via the gtfsrt endpoint\n  street_routing_max_prepost_transit_seconds: 3600 # limit for maxPre/PostTransitTime API params, see below\n  street_routing_max_direct_seconds: 21600 # limit for maxDirectTime API param, high values can lead to long-running, RAM-hungry queries \nlogging:\n  log_level: debug                # log-level (default = debug; Supported log-levels: error, info, debug)\nosr_footpath: true                # enable routing footpaths instead of using transfers from timetable datasets\ngeocoding: true                   # enable geocoding for place/stop name autocompletion\nreverse_geocoding: false          # enable reverse geocoding for mapping a geo coordinate to nearby places/addresses\n```\n\n# Scenario with Elevators\n\nThis 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.\n\n```yml\nserver:\n  web_folder: ui\ntiles:\n  profile: tiles-profiles/full.lua\ngeocoding: true\nstreet_routing: true    # Alternative notion the enable street routing\nosr_footpath: true\nelevators:\n  #  init: fasta.json   # Can be used for debugging, remove `url` key in this case\n  url: https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/facilities\n  headers:\n    DB-Client-ID: b5d28136ffedb73474cc7c97536554df!\n    DB-Api-Key: ef27b9ad8149cddb6b5e8ebb559ce245!\nosm: germany-latest.osm.pbf\ntimetable:\n  extend_missing_footpaths: true\n  use_osm_stop_coordinates: true\n  datasets:\n    de:\n      path: 20250331_fahrplaene_gesamtdeutschland_gtfs.zip\n      rt:\n        - url: https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf\n```\n\n# GBFS Configuration\n\nThis examples shows how to configure multiple GBFS feeds.  \nA 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.\n\n```yaml\ngbfs:\n  feeds:\n    # GBFS feed:\n    callabike:\n      url: https://api.mobidata-bw.de/sharing/gbfs/callabike/gbfs\n    # GBFS manifest / Lamassu feed:\n    mobidata-bw:\n      url: https://api.mobidata-bw.de/sharing/gbfs/v3/manifest.json\n  update_interval: 300\n  http_timeout: 10\n```\n\n## Provider Groups + Colors\n\nGBFS providers (feeds) can be grouped into \"provider groups\". For example, a provider may operate in multiple locations and provide a feed per location.\nTo groups these different feeds into a single provider group, specify the same group name for each feed in the configuration.\n\nFeeds that don't have an explicit group setting in the configuration, their group name is derived from the system name. Group names\nmay not contain commas. The API supports both provider groups and individual providers.\n\nProvider colors are loaded from the feed (`brand_assets.color`) if available, but can also be set in the configuration\nto override the values contained in the feed or to set colors for feeds that don't include color information.\nColors can be set for groups (applies to all providers belonging to the group) or individual providers\n(overrides group color for that feed).\n\n```yaml\ngbfs:\n  feeds:\n    de-CallaBike:\n      url: https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/gbfs\n      color: \"#db0016\"\n    de-VRNnextbike:\n      url: https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_vn/gbfs.json\n      group: nextbike # uses the group color defined below\n    de-NextbikeFrankfurt:\n      url: https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_ff/gbfs.json\n      group: nextbike\n    de-KVV.nextbike:\n      url: https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_fg/gbfs.json\n      group: nextbike\n      color: \"#c30937\" # override color for this particular feed\n  groups:\n    nextbike:\n      # name: nextbike # Optional: Override the name (otherwise the group id, here \"nextbike\", is used)\n      color: \"#0046d6\"\n```\n\nFor aggregated feeds (manifest.json or Lamassu), groups and colors can either be assigned to all providers listed in the aggregated feed\nor individually by using the system_id:\n\n```yaml\ngbfs:\n  feeds:\n    aggregated-single-group:\n      url: https://example.com/one-provider-group/manifest.json\n      group: Example\n      color: \"#db0016\" # or assign a color to the group\n    aggregated-multiple-groups:\n      url: https://example.com/multiple-provider-groups/manifest.json\n      group:\n        source-nextbike-westbike: nextbike # \"source-nextbike-westbike\" is the system_id\n        source-voi-muenster: VOI\n        source-voi-duisburg-oberhausen: VOI\n      # colors can be specified for individual feeds using the same syntax,\n      # but in this example they are defined for the groups below\n      #color:\n      #  \"source-nextbike-westbike\": \"#0046d6\"\n      #  \"source-voi-muenster\": \"#f26961\"\n  groups:\n    nextbike:\n      color: \"#0046d6\"\n    VOI:\n      color: \"#f26961\"\n```\n\n## HTTP Headers + OAuth\n\nIf a feed requires specific HTTP headers, they can be defined like this:\n\n```yaml\ngbfs:\n  feeds:\n    example:\n      url: https://example.com/gbfs\n      headers:\n        authorization: MY_OTHER_API_KEY\n        other-header: other-value\n```\n\nOAuth with client credentials and bearer token types is also supported:\n\n```yaml\ngbfs:\n  feeds:\n    example:\n      url: https://example.com/gbfs\n      oauth:\n        token_url: https://example.com/openid-connect/token\n        client_id: gbfs\n        client_secret: example\n```\n\n## Default Restrictions\n\nA GBFS feed can define geofencing zones and rules, that apply to areas within these zones.\nFor restrictions on areas not included in these geofencing zones, a feed may contain global rules.\nIf these are missing, it's possible to define `default_restrictions`, that apply to either a single feed or a manifest.\nThe following example shows possible configurations:\n\n```yaml\ngbfs:\n  feeds:\n    # GBFS feed:\n    #callabike:\n    #  url: https://api.mobidata-bw.de/sharing/gbfs/callabike/gbfs\n    # GBFS manifest / Lamassu feed:\n    mobidata-bw:\n      url: https://api.mobidata-bw.de/sharing/gbfs/v3/manifest.json\n  default_restrictions:\n    mobidata-bw:callabike: # \"callabike\" feed contained in the \"mobidata-bw\" manifest\n      # these restrictions apply outside of the defined geofencing zones if the feed doesn't contain global rules\n      ride_start_allowed: true\n      ride_end_allowed: true\n      ride_through_allowed: true\n      #station_parking: false\n      #return_constraint: roundtrip_station\n    #mobidata-bw: # default restrictions for all feeds contained in the \"mobidata-bw\" manifest\n    #callabike: # default restrictions for standalone GBFS feed \"callabike\" (when not using the mobidata-bw example)\n  update_interval: 300\n  http_timeout: 10\n```\n\n# Real time protocols\n\nMOTIS supports multiple protocols for real time feeds. This section shows a list of the protocols, including some pitfalls:\n\n| Protocol | `protocol` | Note |\n| ---- | ---- | ---- |\n| GTFS-RT | `gtfsrt` | This is the default, if `protocol` is ommitted. |\n| SIRI Lite (XML) | `siri` | Currently limited to SIRI Lite ET, FM and SX. Still work in progress. Use with care. |\n| SIRI Lite (JSON) | `siri_json` | Same as `siri`, but expects JSON server responses. See below for expected JSON structure. |\n| VDV AUS / VDV454 | `auser` | Requires [`auser`](https://github.com/motis-project/auser) for subscription handling |\n\n## Supported SIRI Lite services\n\nSIRI feeds are divided into multiple feeds called services (check for instance\n[this](https://en.wikipedia.org/wiki/Service_Interface_for_Real_Time_Information#CEN_SIRI_Functional_Services)\nfor a list of all services). Right now MOTIS only supports parsing the\n\"Estimated Timetable\" (ET), the \"Facility Monitoring\" (FM) and the \"Situation\nExchange\" (SX) SIRI services. You can see examples of such feeds\n[here](https://github.com/SIRI-CEN/SIRI/tree/v2.2/examples).\n\nIf you are using the `siri_json` protocol, note that MOTIS expects the\nfollowing JSON structure:\n\n- **Valid** SIRI Lite JSON response:\n\n  ```json\n  {\n    \"ResponseTimestamp\": \"2004-12-17T09:30:46-05:00\",\n    \"ProducerRef\": \"KUBRICK\",\n    \"Status\": true,\n    \"MoreData\": false,\n    \"EstimatedTimetableDelivery\": [\n      ...\n    ]\n  }\n  ```\n\n- **Invalid** SIRI Lite JSON response:\n\n  ```json\n  {\n    \"Siri\": {\n      \"ServiceDelivery\": {\n        \"ResponseTimestamp\": \"2004-12-17T09:30:46-05:00\",\n        \"ProducerRef\": \"KUBRICK\",\n        \"Status\": true,\n        \"MoreData\": false,\n        \"EstimatedTimetableDelivery\": [\n          ...\n        ]\n      }\n    }\n  }\n  ```\n\nIf, as above, the two top keys `\"Siri\"` and `\"ServiceDelivery\"` are included in\nthe JSON response, MOTIS will fail to parse the SIRI Lite feed, throwing\n`[VERIFY FAIL] unable to parse time \"\"` errors.\n\n# Shapes\n\nTo enable shapes support (polylines for trips), `timetable.with_shapes` must\nbe set to `true`. This will load shapes that are present in the datasets\n(e.g. GTFS shapes.txt).\n\nIt is also possible to compute shapes based on OpenStreetMap data. This\nrequires:\n\n- `timetable.with_shapes` set to `true`\n- `osm` data\n- `street_routing` set to `true`\n- `timetable.route_shapes` config:\n\n```yaml\ntimetable:\n  # with_shapes must be set to true to enable shapes support, otherwise no shapes will be loaded or computed\n  with_shapes: true\n  route_shapes: # all these options are optional\n    # available modes:\n    # - all: route shapes for all routes, replace existing shapes from the timetable\n    # - missing: only compute shapes for those routes that don't have existing shapes from the timetable\n    mode: all\n    # routing for specific clasz types can be disabled (default = all enabled)\n    # currently long distance street routing is slow, so in this example\n    # we disable routing shapes for COACH\n    # (if there are shapes for the disabled clasz types in the dataset, these will still be used)\n    clasz:\n      COACH: false\n    # disable shape computation for routes with more than X stops (default = no limit)\n    # (if there are shapes for routes with more than X stops in the dataset, these will still be used)\n    max_stops: 100\n    # limit the number of threads used for shape computation (default = number of hardware threads)\n    n_threads: 6\n    # enable debug API endpoint (default = false)\n    debug_api: true\n    # if you want to use cached shapes even if the osm file has changed since the last import, set this to true (default = false)\n    cache_reuse_old_osm_data: false\n\n    # for debugging purposes, debug information can be written to files\n    # which can be loaded into the debug ui (see osr project)\n    debug:\n      path: /path/to/debug/directory\n      all: false                  # debug all routes\n      all_with_beelines: false    # or only those that include beelines\n      slow: 10000                 # or only those that take >10.000ms to compute\n      # or specific trips/routes:\n      trips:\n        - \"trip_id_1\"\n      route_ids:\n        - \"route_id_1\"\n      route_indices: # these are internal indices (e.g. from debug UI)\n        - 123\n```\n\n## Cache\n\nRouted shapes can be cached to speed up later imports when a timetable dataset\nis updated. If enabled, this will generate an additional cache file. This cache\nfile and the routed shapes data are then reused during import.\n\nNote that old routes are never removed from the routed shapes data files, i.e.,\nthese files grow with every import (unless there are no new routes, in which\ncase the size will stay the same).\nIt is therefore recommended to monitor the size of the \"routed_shapes\\*\" files\nin the data directory.\nThey can safely be deleted before an import, which will cause all shapes that\nare needed for the current datasets to be routed again.\n\nThe cache only applies to routed shapes, not shapes contained in the timetables.\n"
  },
  {
    "path": "docs/windows-dev-setup.md",
    "content": "In the following, we list requirements and a download link. There may be other sources (like package managers) to install these.\n\n- CMake 3.17 (or newer): [cmake.org](https://cmake.org/download/)\n- Git: [git-scm.com](https://git-scm.com/download/win)\n- Visual Studio 2022 or at least \"Build Tools for Visual Studio 2022\": [visualstudio.microsoft.com](https://visualstudio.microsoft.com/de/downloads/)\n- Ninja: [ninja-build.org](https://ninja-build.org/)\n\n> [!CAUTION]\n> Motis' dependency management `pkg` requires that the project is cloned via SSH using an SSH key without a passphrase.\n> See:\n> - [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)\n> - [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)\n\n## Build MOTIS using the command line\n\nStart menu -> `Visual Studio 2022` -> `x64 Native Tools Command Prompt for VS 2022`, then enter:\n\n```bat\ngit clone \"git@github.com:motis-project/motis.git\"\ncd motis\nmkdir build\ncd build\ncmake -GNinja -DCMAKE_BUILD_TYPE=Release ..\nninja\n```\n\n## Build MOTIS using CLion\n\n- Make sure that the architecture is set to `amd64` (Settings -> `Build, Execution, Deployment` -> `Toolchains`).\n- You might have to allow users to create symbolic links. Open `Local Security Policy` and go to `User Rights Assignment`.\n- You might have to enable `Developer Mode` under `Advanced System Settings`.\n"
  },
  {
    "path": "exe/batch.cc",
    "content": "#include <fstream>\n#include <iostream>\n\n#include \"conf/configuration.h\"\n\n#include \"utl/init_from.h\"\n#include \"utl/parallel_for.h\"\n#include \"utl/parser/cstr.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/motis_instance.h\"\n\n#include \"./flags.h\"\n\nnamespace fs = std::filesystem;\nnamespace po = boost::program_options;\nnamespace json = boost::json;\n\nstruct thousands_sep : std::numpunct<char> {\n  char_type do_thousands_sep() const override { return ','; }\n  string_type do_grouping() const override { return \"\\3\"; }\n};\n\nstruct stats {\n  struct entry {\n    bool operator<(entry const& o) const { return value_ < o.value_; }\n    std::uint64_t msg_id_, value_;\n  };\n\n  stats() = default;\n  stats(std::string name, std::uint64_t count_so_far)\n      : name_{std::move(name)}, values_{count_so_far} {}\n\n  void add(uint64_t msg_id, std::uint64_t value) {\n    values_.emplace_back(entry{msg_id, value});\n    sum_ += value;\n  }\n\n  std::string name_;\n  std::vector<entry> values_;\n  std::uint64_t sum_{};\n};\n\nstruct category {\n  category() = default;\n  explicit category(std::string name) : name_(std::move(name)) {}\n\n  std::string name_;\n  std::map<std::string, stats> stats_;\n};\n\nstats::entry quantile(std::vector<stats::entry> const& sorted_values,\n                      double q) {\n  if (q == 1.0) {\n    return sorted_values.back();\n  } else {\n    return sorted_values[std::min(\n        static_cast<std::size_t>(std::round(q * (sorted_values.size() - 1))),\n        sorted_values.size() - 1)];\n  }\n}\n\nvoid print_category(category& cat,\n                    std::uint64_t count,\n                    bool const compact,\n                    int const top) {\n  std::cout << \"\\n\"\n            << cat.name_ << \"\\n\"\n            << std::string(cat.name_.size(), '=') << \"\\n\"\n            << std::endl;\n  for (auto& s : cat.stats_) {\n    auto& stat = s.second;\n    if (stat.values_.empty()) {\n      continue;\n    }\n    utl::sort(stat.values_);\n    auto const avg = (stat.sum_ / static_cast<double>(count));\n    if (compact) {\n      std::cout << std::left << std::setw(30) << stat.name_\n                << \" avg: \" << std::setw(27) << std::setprecision(4)\n                << std::fixed << avg << \" Q(99): \" << std::setw(25)\n                << quantile(stat.values_, 0.99).value_\n                << \" Q(90): \" << std::setw(22)\n                << quantile(stat.values_, 0.9).value_\n                << \" Q(80): \" << std::setw(22)\n                << quantile(stat.values_, 0.8).value_\n                << \" Q(50): \" << std::setw(22)\n                << quantile(stat.values_, 0.5).value_;\n\n      auto const from = static_cast<std::uint64_t>(\n          std::max(static_cast<std::int64_t>(0L),\n                   static_cast<std::int64_t>(stat.values_.size()) -\n                       static_cast<std::int64_t>(top)));\n      for (auto i = from; i != stat.values_.size(); ++i) {\n        auto const i_rev = stat.values_.size() - (i - from) - 1;\n        std::cout << \"(v=\" << stat.values_[i_rev].value_\n                  << \", i=\" << stat.values_[i_rev].msg_id_ << \")\";\n        if (i != stat.values_.size() - 1) {\n          std::cout << \", \";\n        }\n      }\n      std::cout << std::endl;\n    } else {\n      std::cout\n          << stat.name_ << \"\\n      average: \" << std::right << std::setw(15)\n          << std::setprecision(2) << std::fixed << avg\n          << \"\\n          max: \" << std::right << std::setw(12)\n          << std::max_element(begin(stat.values_), end(stat.values_))->value_\n          << \"\\n  99 quantile: \" << std::right << std::setw(12)\n          << quantile(stat.values_, 0.99).value_\n          << \"\\n  90 quantile: \" << std::right << std::setw(12)\n          << quantile(stat.values_, 0.9).value_\n          << \"\\n  80 quantile: \" << std::right << std::setw(12)\n          << quantile(stat.values_, 0.8).value_\n          << \"\\n  50 quantile: \" << std::right << std::setw(12)\n          << quantile(stat.values_, 0.5).value_\n          << \"\\n          min: \" << std::right << std::setw(12)\n          << std::min_element(begin(stat.values_), end(stat.values_))->value_\n          << \"\\n\"\n          << std::endl;\n    }\n  }\n}\n\nnamespace motis {\n\nint batch(int ac, char** av) {\n  auto data_path = fs::path{\"data\"};\n  auto queries_path = fs::path{\"queries.txt\"};\n  auto responses_path = fs::path{\"responses.txt\"};\n  auto mt = true;\n\n  auto desc = po::options_description{\"Options\"};\n  desc.add_options()  //\n      (\"help\", \"Prints this help message\")  //\n      (\"multithreading,mt\", po::value(&mt)->default_value(mt))  //\n      (\"queries,q\", po::value(&queries_path)->default_value(queries_path),\n       \"queries file\")  //\n      (\"responses,r\", po::value(&responses_path)->default_value(responses_path),\n       \"response file\");\n  add_data_path_opt(desc, data_path);\n\n  auto vm = parse_opt(ac, av, desc);\n  if (vm.count(\"help\")) {\n    std::cout << desc << \"\\n\";\n    return 0;\n  }\n\n  auto queries = std::vector<std::string_view>{};\n  auto f = cista::mmap{queries_path.generic_string().c_str(),\n                       cista::mmap::protection::READ};\n  utl::for_each_line(utl::cstr{f.view()},\n                     [&](utl::cstr s) { queries.push_back(s.view()); });\n\n  auto const c = config::read(data_path / \"config.yml\");\n  utl::verify(c.timetable_.has_value(), \"timetable required\");\n\n  auto d = data{data_path, c};\n  utl::verify(d.tt_, \"timetable required\");\n\n  auto response_time = stats{\"response_time\", 0U};\n\n  struct state {};\n\n  auto out = std::ofstream{responses_path};\n  auto m = motis_instance{net::default_exec{}, d, c, \"\"};\n  auto const compute_response = [&](state&, std::size_t const id) {\n    UTL_START_TIMING(request);\n    auto response = std::string{};\n    try {\n      m.qr_(\n          {boost::beast::http::verb::get,\n           boost::beast::string_view{queries.at(id)}, 11},\n          [&](net::web_server::http_res_t const& res) {\n            std::visit(\n                [&](auto&& r) {\n                  using ResponseType = std::decay_t<decltype(r)>;\n                  if constexpr (std::is_same_v<ResponseType,\n                                               net::web_server::string_res_t>) {\n                    response = r.body();\n                    if (response.empty()) {\n                      std::cout << \"empty response for \" << id << \": \"\n                                << queries.at(id) << \" [status=\" << r.result()\n                                << \"]\\n\";\n                    }\n                  } else {\n                    throw utl::fail(\"not a valid response type: {}\",\n                                    cista::type_str<ResponseType>());\n                  }\n                },\n                res);\n          },\n          false);\n    } catch (std::exception const& e) {\n      std::cerr << \"ERROR IN QUERY \" << id << \": \" << e.what() << \"\\n\";\n    }\n    return std::pair{UTL_GET_TIMING_MS(request), std::move(response)};\n  };\n\n  auto const pt = utl::activate_progress_tracker(\"batch\");\n  pt->in_high(queries.size());\n  if (mt) {\n    utl::parallel_ordered_collect_threadlocal<state>(\n        queries.size(), compute_response,\n        [&](std::size_t const id,\n            std::pair<std::uint64_t, std::string> const& s) {\n          response_time.add(id, s.first);\n          out << s.second << \"\\n\";\n        },\n        pt->update_fn());\n  } else {\n    auto s = state{};\n    for (auto i = 0U; i != queries.size(); ++i) {\n      compute_response(s, i);\n      pt->increment();\n    }\n  }\n\n  auto cat = category{};\n  cat.name_ = \"response_time\";\n  cat.stats_.emplace(\"response_time\", std::move(response_time));\n  std::cout.imbue(std::locale(std::locale::classic(), new thousands_sep));\n  print_category(cat, queries.size(), false, 10U);\n\n  return 0U;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "exe/compare.cc",
    "content": "#include <fstream>\n#include <iostream>\n#include <ranges>\n#include <string>\n#include <vector>\n\n#include \"conf/configuration.h\"\n\n#include \"boost/json/parse.hpp\"\n#include \"boost/json/serialize.hpp\"\n#include \"boost/json/value_from.hpp\"\n#include \"boost/json/value_to.hpp\"\n\n#include \"fmt/std.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/file_utils.h\"\n#include \"utl/get_or_create.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/overloaded.h\"\n#include \"utl/sorted_diff.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/verify.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/types.h\"\n\n#include \"./flags.h\"\n\nnamespace fs = std::filesystem;\nnamespace po = boost::program_options;\nnamespace json = boost::json;\n\nnamespace motis {\n\nint compare(int ac, char** av) {\n  auto subset_check = false;\n  auto queries_path = fs::path{\"queries.txt\"};\n  auto responses_paths = std::vector<std::string>{};\n  auto fails_path = fs::path{\"fail\"};\n  auto desc = po::options_description{\"Options\"};\n  desc.add_options()  //\n      (\"help\", \"Prints this help message\")  //\n      (\"queries,q\", po::value(&queries_path)->default_value(queries_path),\n       \"queries file\")  //\n      (\"subset_check\", po::value(&subset_check)->default_value(subset_check),\n       \"only check subset ([1...N] <= [0])\")  //\n      (\"responses,r\",\n       po::value(&responses_paths)\n           ->multitoken()\n           ->default_value(responses_paths),\n       \"response files\");\n\n  auto vm = parse_opt(ac, av, desc);\n  if (vm.count(\"help\")) {\n    std::cout << desc << \"\\n\";\n    return 0;\n  }\n\n  auto const write_fails = fs::is_directory(fails_path);\n  if (!write_fails) {\n    fmt::println(\"{} is not a directory, not writing fails\", fails_path);\n  }\n\n  struct info {\n    unsigned id_;\n    std::optional<api::plan_params> params_{};\n    std::vector<std::optional<api::plan_response>> responses_{};\n  };\n  auto const params = [](api::Itinerary const& x) {\n    return std::tie(x.startTime_, x.endTime_, x.transfers_);\n  };\n  auto const equal = [&](std::vector<api::Itinerary> const& a,\n                         std::vector<api::Itinerary> const& b) {\n    if (subset_check) {\n      return utl::all_of(a, [&](api::Itinerary const& x) {\n        return utl::any_of(\n            b, [&](api::Itinerary const& y) { return params(x) == params(y); });\n      });\n    } else {\n      return std::ranges::equal(a | std::views::transform(params),\n                                b | std::views::transform(params));\n    }\n  };\n  auto const print_params = [](api::Itinerary const& x) {\n    std::cout << x.startTime_ << \", \" << x.endTime_\n              << \", transfers=\" << std::setw(2) << std::left << x.transfers_;\n  };\n  auto const print_none = []() { std::cout << \"\\t\\t\\t\\t\\t\\t\"; };\n  auto n_equal = 0U;\n  auto const print_differences = [&](info const& x) {\n    auto const is_incomplete =\n        utl::any_of(x.responses_, [](auto&& x) { return !x.has_value(); });\n\n    auto const ref =\n        x.responses_[0].value_or(api::plan_response{}).itineraries_;\n    auto mismatch = false;\n    for (auto i = 1U; i < x.responses_.size(); ++i) {\n      mismatch |= !x.responses_[i].has_value();\n\n      auto const uut =\n          x.responses_[i].value_or(api::plan_response{}).itineraries_;\n      if (equal(ref, uut)) {\n        ++n_equal;\n        continue;\n      }\n\n      mismatch = true;\n      std::cout << \"QUERY=\" << x.id_ << \" [\"\n                << x.params_->to_url(\"/api/v1/plan\") << \"]\";\n      if (is_incomplete) {\n        std::cout << \" [INCOMPLETE!!]\";\n      }\n      std::cout << \"\\n\";\n      utl::sorted_diff(\n          ref, uut,\n          [&](api::Itinerary const& a, api::Itinerary const& b) {\n            return params(a) < params(b);\n          },\n          [&](api::Itinerary const&, api::Itinerary const&) {\n            return false;  // always call for equal\n          },\n          utl::overloaded{\n              [&](utl::op op, api::Itinerary const& j) {\n                if (op == utl::op::kAdd) {\n                  print_none();\n                  std::cout << \"\\t\\t\\t\\t\";\n                  print_params(j);\n                  std::cout << \"\\n\";\n                } else {\n                  print_params(j);\n                  std::cout << \"\\t\\t\\t\\t\";\n                  print_none();\n                  std::cout << \"\\n\";\n                }\n              },\n              [&](api::Itinerary const& a, api::Itinerary const& b) {\n                print_params(a);\n                std::cout << \"\\t\\t\\t\";\n                print_params(b);\n                std::cout << \"\\n\";\n              }});\n      std::cout << \"\\n\\n\";\n    }\n\n    if (mismatch && write_fails) {\n      std::ofstream{fails_path / fmt::format(\"{}_q.txt\", x.id_)}\n          << x.params_->to_url(\"/api/v1/plan\") << \"\\n\";\n      for (auto i = 0U; i < x.responses_.size(); ++i) {\n        if (!x.responses_[i].has_value()) {\n          continue;\n        }\n        std::ofstream{fails_path / fmt::format(\"{}_{}.json\", x.id_, i)}\n            << json::serialize(json::value_from(x.responses_[i].value()))\n            << \"\\n\";\n      }\n    }\n  };\n\n  auto query_file = utl::open_file(queries_path);\n  auto responses_files =\n      utl::to_vec(responses_paths, [&](auto&& p) { return utl::open_file(p); });\n\n  auto n_consumed = 0U;\n  auto query_id = 0U;\n  while (true) {\n    auto nfo =\n        info{.id_ = ++query_id,\n             .responses_ = std::vector<std::optional<api::plan_response>>{\n                 responses_files.size()}};\n\n    if (auto const q = utl::read_line(query_file); q.has_value()) {\n      nfo.params_ = api::plan_params{boost::urls::url{*q}.params()};\n    } else {\n      break;\n    }\n\n    for (auto const [i, res_file] : utl::enumerate(responses_files)) {\n      if (auto const r = utl::read_line(res_file); r.has_value()) {\n        try {\n          auto val = boost::json::parse(*r);\n          if (val.is_object() &&\n              val.as_object().contains(\"requestParameters\")) {\n            auto res = json::value_to<api::plan_response>(val);\n            utl::sort(res.itineraries_, [&](auto&& a, auto&& b) {\n              return params(a) < params(b);\n            });\n            nfo.responses_[i] = std::move(res);\n          }\n        } catch (...) {\n        }\n      } else {\n        break;\n      }\n    }\n\n    print_differences(nfo);\n    ++n_consumed;\n  }\n\n  std::cout << \"consumed: \" << n_consumed << \"\\n\";\n  std::cout << \"   equal: \" << n_equal << \"\\n\";\n\n  return n_consumed == n_equal ? 0 : 1;\n}\n\n}  // namespace motis"
  },
  {
    "path": "exe/extract.cc",
    "content": "#include <filesystem>\n#include <iterator>\n\n#include \"boost/json.hpp\"\n#include \"boost/program_options.hpp\"\n\n#include \"fmt/ranges.h\"\n#include \"fmt/std.h\"\n\n#include \"utl/parser/buf_reader.h\"\n#include \"utl/parser/csv_range.h\"\n#include \"utl/parser/split.h\"\n#include \"utl/pipes.h\"\n#include \"utl/progress_tracker.h\"\n#include \"utl/verify.h\"\n\n#include \"nigiri/loader/dir.h\"\n#include \"nigiri/common/interval.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/types.h\"\n\n#include \"flags.h\"\n\nnamespace po = boost::program_options;\nnamespace fs = std::filesystem;\nnamespace n = nigiri;\n\nnamespace motis {\n\nvoid copy_stop_times(hash_set<std::string> const& trip_ids,\n                     hash_set<std::string> const& filter_stop_ids,\n                     std::string_view file_content,\n                     hash_set<std::string>& stop_ids,\n                     std::ostream& out) {\n  struct csv_stop_time {\n    utl::csv_col<utl::cstr, UTL_NAME(\"trip_id\")> trip_id_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"stop_id\")> stop_id_;\n  };\n\n  auto n_lines = 0U;\n  auto reader = utl::make_buf_reader(file_content);\n  auto line = reader.read_line();\n  auto const header_permutation = utl::read_header<csv_stop_time>(line);\n  out << line.view() << \"\\n\";\n  while ((line = reader.read_line())) {\n    auto const row = utl::read_row<csv_stop_time>(header_permutation, line);\n    if (trip_ids.contains(row.trip_id_->view()) &&\n        (filter_stop_ids.empty() ||\n         filter_stop_ids.contains(row.stop_id_->view()))) {\n      stop_ids.insert(row.stop_id_->view());\n      out << line.view() << \"\\n\";\n      ++n_lines;\n    }\n  }\n\n  fmt::println(\"  stop_times.txt: lines written: {}\", n_lines);\n}\n\nvoid copy_stops(hash_set<std::string>& stop_ids,\n                std::string_view file_content,\n                std::ostream& out,\n                bool const filter_stops) {\n  struct csv_stop {\n    utl::csv_col<utl::cstr, UTL_NAME(\"stop_id\")> stop_id_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"parent_station\")> parent_station_;\n  };\n\n  {  // First pass: collect parents.\n    auto reader = utl::make_buf_reader(file_content);\n    auto line = reader.read_line();\n    auto const header_permutation = utl::read_header<csv_stop>(line);\n    while ((line = reader.read_line())) {\n      auto const row = utl::read_row<csv_stop>(header_permutation, line);\n      if (!row.parent_station_->empty() &&\n          stop_ids.contains(row.stop_id_->view())) {\n        stop_ids.emplace(row.parent_station_->view());\n      }\n    }\n  }\n\n  {  // Second pass: copy contents.\n    auto n_lines = 0U;\n    auto reader = utl::make_buf_reader(file_content);\n    auto line = reader.read_line();\n    auto const header_permutation = utl::read_header<csv_stop>(line);\n    out << line.view() << \"\\n\";\n    while ((line = reader.read_line())) {\n      if (filter_stops) {\n        auto const row = utl::read_row<csv_stop>(header_permutation, line);\n        if (stop_ids.contains(row.stop_id_->view())) {\n          out << line.view() << \"\\n\";\n          ++n_lines;\n        }\n      } else {\n        out << line.view() << \"\\n\";\n      }\n    }\n    fmt::println(\"  stops.txt: lines written: {}\", n_lines);\n  }\n}\n\nvoid copy_trips(hash_set<std::string> const& trip_ids,\n                std::string_view file_content,\n                hash_set<std::string>& route_ids,\n                hash_set<std::string>& service_ids,\n                std::ostream& out) {\n  struct csv_trip {\n    utl::csv_col<utl::cstr, UTL_NAME(\"trip_id\")> trip_id_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"route_id\")> route_id_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"service_id\")> service_id_;\n  };\n\n  auto n_lines = 0U;\n  auto reader = utl::make_buf_reader(file_content);\n  auto line = reader.read_line();\n  auto const header_permutation = utl::read_header<csv_trip>(line);\n  out << line.view() << \"\\n\";\n  while ((line = reader.read_line())) {\n    auto const row = utl::read_row<csv_trip>(header_permutation, line);\n    if (trip_ids.contains(row.trip_id_->view())) {\n      route_ids.insert(row.route_id_->view());\n      service_ids.insert(row.service_id_->view());\n      out << line.view() << \"\\n\";\n      ++n_lines;\n    }\n  }\n\n  fmt::println(\"  trips.txt: lines written: {}\", n_lines);\n}\n\nvoid copy_calendar(hash_set<std::string> const& service_ids,\n                   std::string_view file_content,\n                   std::ostream& out) {\n  struct csv_service {\n    utl::csv_col<utl::cstr, UTL_NAME(\"service_id\")> service_id_;\n  };\n\n  auto n_lines = 0U;\n  auto reader = utl::make_buf_reader(file_content);\n  auto line = reader.read_line();\n  auto const header_permutation = utl::read_header<csv_service>(line);\n  out << line.view() << \"\\n\";\n  while ((line = reader.read_line())) {\n    auto const row = utl::read_row<csv_service>(header_permutation, line);\n    if (service_ids.contains(row.service_id_->view())) {\n      out << line.view() << \"\\n\";\n      ++n_lines;\n    }\n  }\n\n  fmt::println(\"  calendar.txt / calendar_dates.txt: lines written: {}\",\n               n_lines);\n}\n\nvoid copy_routes(hash_set<std::string> const& route_ids,\n                 std::string_view file_content,\n                 hash_set<std::string>& agency_ids,\n                 std::ostream& out) {\n  struct csv_service {\n    utl::csv_col<utl::cstr, UTL_NAME(\"route_id\")> route_id_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"agency_id\")> agency_id_;\n  };\n\n  auto n_lines = 0U;\n  auto reader = utl::make_buf_reader(file_content);\n  auto line = reader.read_line();\n  auto const header_permutation = utl::read_header<csv_service>(line);\n  out << line.view() << \"\\n\";\n  while ((line = reader.read_line())) {\n    auto const row = utl::read_row<csv_service>(header_permutation, line);\n    if (route_ids.contains(row.route_id_->view())) {\n      agency_ids.insert(row.agency_id_->view());\n      out << line.view() << \"\\n\";\n      ++n_lines;\n    }\n  }\n\n  fmt::println(\"  routes.txt: lines written: {}\", n_lines);\n}\n\nvoid copy_agencies(hash_set<std::string> const& agency_ids,\n                   std::string_view file_content,\n                   std::ostream& out) {\n  struct csv_stop {\n    utl::csv_col<utl::cstr, UTL_NAME(\"agency_id\")> agency_id_;\n  };\n\n  auto n_lines = 0U;\n  auto reader = utl::make_buf_reader(file_content);\n  auto line = reader.read_line();\n  auto const header_permutation = utl::read_header<csv_stop>(line);\n  out << line.view() << \"\\n\";\n  while ((line = reader.read_line())) {\n    auto const row = utl::read_row<csv_stop>(header_permutation, line);\n    if (agency_ids.contains(row.agency_id_->view())) {\n      out << line.view() << \"\\n\";\n      ++n_lines;\n    }\n  }\n\n  fmt::println(\"  agencies.txt: lines written: {}\", n_lines);\n}\n\nvoid copy_transfers(hash_set<std::string> const& stop_ids,\n                    std::string_view file_content,\n                    std::ostream& out) {\n  struct csv_stop {\n    utl::csv_col<utl::cstr, UTL_NAME(\"from_stop_id\")> from_stop_id_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"to_stop_id\")> to_stop_id_;\n  };\n\n  auto n_lines = 0U;\n  auto reader = utl::make_buf_reader(file_content);\n  auto line = reader.read_line();\n  auto const header_permutation = utl::read_header<csv_stop>(line);\n  out << line.view() << \"\\n\";\n  while ((line = reader.read_line())) {\n    auto const row = utl::read_row<csv_stop>(header_permutation, line);\n    if (stop_ids.contains(row.from_stop_id_->view()) &&\n        stop_ids.contains(row.to_stop_id_->view())) {\n      out << line.view() << \"\\n\";\n      ++n_lines;\n    }\n  }\n\n  fmt::println(\"  transfers.txt: lines written: {}\", n_lines);\n}\n\nint extract(int ac, char** av) {\n  auto in = std::vector<fs::path>{\"response.json\"};\n  auto out = fs::path{\"gtfs\"};\n  auto reduce = false;\n  auto filter_stops = true;\n  auto desc = po::options_description{\"Options\"};\n  desc.add_options()  //\n      (\"help\", \"Prints this help message\")  //\n      (\"reduce\", po::value(&reduce)->default_value(reduce),\n       \"Only extract first and last stop of legs for stop times\")  //\n      (\"filter_stops\", po::value(&filter_stops)->default_value(filter_stops),\n       \"Filter stops\")  //\n      (\"in,i\", po::value(&in)->multitoken(),\n       \"PlanResponse JSON input files\")  //\n      (\"out,o\", po::value(&out), \"output directory\");\n  auto vm = parse_opt(ac, av, desc);\n\n  if (vm.count(\"help\")) {\n    std::cout << desc << \"\\n\";\n    return 0;\n  }\n\n  auto important_stops = hash_set<std::string>{};\n  auto const add_important_stop = [&](api::Place const& p) {\n    if (!reduce || p.vertexType_ != api::VertexTypeEnum::TRANSIT) {\n      return;\n    }\n    auto const tag_end = p.stopId_.value().find('_');\n    utl::verify(tag_end != std::string::npos, \"no tag found for stop id {}\",\n                p.stopId_.value());\n    auto const [_, added] =\n        important_stops.insert(p.stopId_.value().substr(tag_end + 1U));\n    if (added) {\n      fmt::println(\"important stop {}\", p.stopId_.value().substr(tag_end + 1U));\n    }\n  };\n\n  auto todos = hash_map<std::string, hash_set<std::string>>{};\n  auto source = std::string{};\n  auto from_line = std::string{};\n  auto to_line = std::string{};\n  auto path = std::string{};\n  for (auto const& x : in) {\n    auto const f =\n        cista::mmap{x.generic_string().c_str(), cista::mmap::protection::READ};\n    auto const res =\n        boost::json::value_to<api::plan_response>(boost::json::parse(f.view()));\n\n    fmt::println(\"found {} itineraries\", res.itineraries_.size());\n\n    for (auto const& i : res.itineraries_) {\n      for (auto const& l : i.legs_) {\n        add_important_stop(l.from_);\n        add_important_stop(l.to_);\n\n        if (!l.source_.has_value() || !l.tripId_.has_value()) {\n          continue;\n        }\n\n        source.resize(l.source_->size());\n        std::reverse_copy(begin(*l.source_), end(*l.source_), begin(source));\n        auto const [to, from, p] =\n            utl::split<':', utl::cstr, utl::cstr, utl::cstr>(source);\n\n        from_line.resize(from.length());\n        std::reverse_copy(begin(from), end(from), begin(from_line));\n\n        to_line.resize(to.length());\n        std::reverse_copy(begin(to), end(to), begin(to_line));\n\n        path.resize(p.length());\n        std::reverse_copy(begin(p), end(p), begin(path));\n\n        auto const trip_id = split_trip_id(*l.tripId_);\n\n        auto const [_, added] = todos[path].emplace(trip_id.trip_id_);\n        if (added) {\n          fmt::println(\"added {}:{}:{}, trip_id={}\", path, from_line, to_line,\n                       trip_id.trip_id_);\n        }\n      }\n    }\n  }\n\n  auto stop_ids = hash_set<std::string>{};\n  auto route_ids = hash_set<std::string>{};\n  auto service_ids = hash_set<std::string>{};\n  auto agency_ids = hash_set<std::string>{};\n  for (auto const& [stop_times_str, trip_ids] : todos) {\n    auto const stop_times_path = fs::path{stop_times_str};\n\n    stop_ids.clear();\n    route_ids.clear();\n    service_ids.clear();\n    agency_ids.clear();\n\n    utl::verify(stop_times_path.filename() == \"stop_times.txt\",\n                \"expected filename stop_times.txt, got \\\"{}\\\"\",\n                stop_times_path);\n\n    auto const dataset_dir = stop_times_path.parent_path();\n    utl::verify(stop_times_path.has_parent_path() && fs::exists(dataset_dir),\n                \"expected path \\\"{}\\\" to have existent parent path\",\n                stop_times_path);\n\n    auto const dir = n::loader::make_dir(dataset_dir);\n    utl::verify(dir->exists(\"stop_times.txt\"),\n                \"no stop_times.txt file found in {}\", dataset_dir);\n\n    auto ec = std::error_code{};\n    fs::create_directories(out / dataset_dir.filename(), ec);\n\n    {\n      fmt::println(\"writing {}/stop_times.txt, searching for trips={}\",\n                   out / dataset_dir.filename(), trip_ids);\n      auto of = std::ofstream{out / dataset_dir.filename() / \"stop_times.txt\"};\n      fmt::println(\"important stops: {}\", important_stops);\n      copy_stop_times(trip_ids, important_stops,\n                      dir->get_file(\"stop_times.txt\").data(), stop_ids, of);\n    }\n\n    {\n      fmt::println(\"writing {}/stops.txt, searching for stops={}\",\n                   out / dataset_dir.filename(), stop_ids);\n      auto of = std::ofstream{out / dataset_dir.filename() / \"stops.txt\"};\n      copy_stops(stop_ids, dir->get_file(\"stops.txt\").data(), of, filter_stops);\n    }\n\n    {\n      fmt::println(\"writing {}/trips.txt\", out / dataset_dir.filename());\n      auto of = std::ofstream{out / dataset_dir.filename() / \"trips.txt\"};\n      copy_trips(trip_ids, dir->get_file(\"trips.txt\").data(), route_ids,\n                 service_ids, of);\n    }\n\n    {\n      fmt::println(\"writing {}/routes.txt, searching for routes={}\",\n                   out / dataset_dir.filename(), route_ids);\n      auto of = std::ofstream{out / dataset_dir.filename() / \"routes.txt\"};\n      copy_routes(route_ids, dir->get_file(\"routes.txt\").data(), agency_ids,\n                  of);\n    }\n\n    if (dir->exists(\"calendar.txt\")) {\n      fmt::println(\"writing {}/calendar.txt, searching for service_ids={}\",\n                   out / dataset_dir.filename(), service_ids);\n      auto of = std::ofstream{out / dataset_dir.filename() / \"calendar.txt\"};\n      copy_calendar(service_ids, dir->get_file(\"calendar.txt\").data(), of);\n    }\n\n    if (dir->exists(\"calendar_dates.txt\")) {\n      fmt::println(\n          \"writing {}/calendar_dates.txt, searching for service_ids={}\",\n          out / dataset_dir.filename(), service_ids);\n      auto of =\n          std::ofstream{out / dataset_dir.filename() / \"calendar_dates.txt\"};\n      copy_calendar(service_ids, dir->get_file(\"calendar_dates.txt\").data(),\n                    of);\n    }\n\n    if (dir->exists(\"agency.txt\")) {\n      fmt::println(\"writing {}/agency.txt, searching for agencies={}\",\n                   out / dataset_dir.filename(), agency_ids);\n      auto of = std::ofstream{out / dataset_dir.filename() / \"agency.txt\"};\n      copy_agencies(agency_ids, dir->get_file(\"agency.txt\").data(), of);\n    }\n\n    if (dir->exists(\"transfers.txt\")) {\n      fmt::println(\"writing {}/transfers.txt\", out / dataset_dir.filename());\n      auto of = std::ofstream{out / dataset_dir.filename() / \"transfers.txt\"};\n      copy_transfers(stop_ids, dir->get_file(\"transfers.txt\").data(), of);\n    }\n\n    if (dir->exists(\"feed_info.txt\")) {\n      std::ofstream{out / dataset_dir.filename() / \"feed_info.txt\"}\n          << dir->get_file(\"feed_info.txt\").data();\n    }\n  }\n\n  return 0;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "exe/flags.h",
    "content": "#pragma once\n\n#include <filesystem>\n#include <string>\n#include <vector>\n\n#include \"boost/program_options.hpp\"\n\nnamespace motis {\n\ninline void add_help_opt(boost::program_options::options_description& desc) {\n  desc.add_options()(\"help,h\", \"print this help message\");\n}\n\ninline void add_data_path_opt(boost::program_options::options_description& desc,\n                              std::filesystem::path& p) {\n  desc.add_options()  //\n      (\"data,d\", boost::program_options::value(&p)->default_value(p),\n       \"The data path contains all preprocessed data as well as a \"\n       \"`config.yml`. \"\n       \"It will be created by the `motis import` command. After the import has \"\n       \"finished, `motis server` only needs the `data` folder and can run \"\n       \"without the input files (such as OpenStreetMap file, GTFS datasets, \"\n       \"tiles-profiles, etc.)\");\n}\n\ninline void add_config_path_opt(\n    boost::program_options::options_description& desc,\n    std::filesystem::path& p) {\n  desc.add_options()  //\n      (\"config,c\", boost::program_options::value(&p)->default_value(p),\n       \"Configuration YAML file. Legacy INI files are still supported but this \"\n       \"support will be dropped in the future.\");\n}\n\ninline void add_trip_id_opt(boost::program_options::options_description& desc) {\n  desc.add_options()(\n      \"trip-id,t\",\n      boost::program_options::value<std::vector<std::string>>()->composing(),\n      \"Add trip-id to analyze.\\n\"\n      \"If the trip-id is encoded, it will be decoded automatically.\\n\"\n      \"This option can be used multiple times.\\n\"\n      \"\\n\"\n      \"Will search the shape corresponding to each trip-id. \"\n      \"If a shape is found, the index of the shape point, that is \"\n      \"matched with each stop, will be printed.\\n\"\n      \"Notice that the first and last stop of a trip will always be \"\n      \"matched with the first and last shape point respectively.\\n\"\n      \"If a shape contains less points than stops in the trip, this \"\n      \"segmentation is not possible.\");\n}\n\ninline void add_log_level_opt(boost::program_options::options_description& desc,\n                              std::string& log_lvl) {\n  desc.add_options()(\"log-level\", boost::program_options::value(&log_lvl),\n                     \"Set the log level.\\n\"\n                     \"Supported log levels: error, info, debug\");\n}\n\ninline boost::program_options::variables_map parse_opt(\n    int ac, char** av, boost::program_options::options_description& desc) {\n  namespace po = boost::program_options;\n  auto vm = po::variables_map{};\n  po::store(po::command_line_parser(ac, av).options(desc).run(), vm);\n  po::notify(vm);\n  return vm;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "exe/generate.cc",
    "content": "#include <fstream>\n#include <iostream>\n#include <mutex>\n\n#include \"conf/configuration.h\"\n\n#include \"boost/url/url.hpp\"\n\n#include \"nigiri/common/interval.h\"\n#include \"nigiri/routing/raptor/debug.h\"\n#include \"nigiri/routing/search.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"utl/progress_tracker.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/odm/bounds.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/tag_lookup.h\"\n\n#include \"./flags.h\"\n\nnamespace n = nigiri;\nnamespace fs = std::filesystem;\nnamespace po = boost::program_options;\n\nnamespace motis {\n\nconstexpr auto const kMinRank = 16UL;\n\nstatic std::atomic_uint32_t seed{0U};\n\nstd::uint32_t rand_in(std::uint32_t const from, std::uint32_t const to) {\n  auto a = ++seed;\n  a = (a ^ 61U) ^ (a >> 16U);\n  a = a + (a << 3U);\n  a = a ^ (a >> 4U);\n  a = a * 0x27d4eb2d;\n  a = a ^ (a >> 15U);\n  return from + (a % (to - from));\n}\n\ntemplate <typename It>\nIt rand_in(It const begin, It const end) {\n  return std::next(\n      begin,\n      rand_in(0U, static_cast<std::uint32_t>(std::distance(begin, end))));\n}\n\ntemplate <typename Collection>\nCollection::value_type rand_in(Collection const& c) {\n  using std::begin;\n  using std::end;\n  utl::verify(!c.empty(), \"empty collection\");\n  return *rand_in(begin(c), end(c));\n}\n\nn::location_idx_t random_stop(n::timetable const& tt,\n                              std::vector<n::location_idx_t> const& stops) {\n  auto s = n::location_idx_t::invalid();\n  do {\n    s = rand_in(stops);\n  } while (tt.location_routes_[s].empty());\n  return s;\n}\n\nint generate(int ac, char** av) {\n  auto data_path = fs::path{\"data\"};\n  auto n = 100U;\n  auto first_day = std::optional<date::sys_days>{};\n  auto last_day = std::optional<date::sys_days>{};\n  auto time_of_day = std::optional<std::uint32_t>{};\n  auto modes = std::optional<std::vector<api::ModeEnum>>{};\n  auto max_dist = 800.0;  // m\n  auto use_walk = false;\n  auto use_bike = false;\n  auto use_car = false;\n  auto use_odm = false;\n  auto lb_rank = true;\n  auto p = api::plan_params{};\n\n  auto const parse_date = [](std::string_view const s) {\n    std::stringstream in;\n    in.exceptions(std::ios::badbit | std::ios::failbit);\n    in << s;\n    auto d = date::sys_days{};\n    in >> date::parse(\"%Y-%m-%d\", d);\n    return d;\n  };\n\n  auto const parse_first_day = [&](std::string_view const s) {\n    first_day = parse_date(s);\n  };\n\n  auto const parse_last_day = [&](std::string_view const s) {\n    last_day = parse_date(s);\n  };\n\n  auto const parse_modes = [&](std::string_view const s) {\n    modes = std::vector<api::ModeEnum>{};\n    if (s.contains(\"WALK\")) {\n      modes->emplace_back(api::ModeEnum::WALK);\n      use_walk = true;\n    }\n    if (s.contains(\"BIKE\")) {\n      modes->emplace_back(api::ModeEnum::BIKE);\n      use_bike = true;\n    }\n    if (s.contains(\"CAR\")) {\n      modes->emplace_back(api::ModeEnum::CAR);\n      use_car = true;\n    }\n    if (s.contains(\"ODM\")) {\n      modes->emplace_back(api::ModeEnum::ODM);\n      use_odm = true;\n    }\n    if (s.contains(\"RIDE_SHARING\")) {\n      modes->emplace_back(api::ModeEnum::RIDE_SHARING);\n      use_odm = true;\n    }\n  };\n\n  auto const parse_time_of_day = [&](std::uint32_t const h) {\n    time_of_day = h % 24U;\n  };\n\n  auto desc = po::options_description{\"Options\"};\n  desc.add_options()  //\n      (\"help\", \"Prints this help message\")  //\n      (\"n,n\", po::value(&n)->default_value(n), \"number of queries\")  //\n      (\"first_day\", po::value<std::string>()->notifier(parse_first_day),\n       \"first day of query generation, format: YYYY-MM-DD\")  //\n      (\"last_day\", po::value<std::string>()->notifier(parse_last_day),\n       \"last day of query generation, format: YYYY-MM-DD\")  //\n      (\"time_of_day\", po::value<std::uint32_t>()->notifier(parse_time_of_day),\n       \"fixes the time of day of all queries to the given number of hours \"\n       \"after midnight, i.e., 0 - 23\")  //\n      (\"modes,m\", po::value<std::string>()->notifier(parse_modes),\n       \"comma-separated list of modes for first/last mile and \"\n       \"direct (requires \"\n       \"street routing), supported: WALK, BIKE, CAR, ODM\")  //\n      (\"all,a\",\n       \"requires OSM nodes to be accessible by all specified modes, otherwise \"\n       \"OSM nodes accessible by at least one mode are eligible, only used for \"\n       \"intermodal queries\")  //\n      (\"max_dist\", po::value(&max_dist)->default_value(max_dist),\n       \"maximum distance from a public transit stop in meters, only used for \"\n       \"intermodal queries\")  //\n      (\"max_travel_time\",\n       po::value<std::int64_t>()->notifier(\n           [&](auto const v) { p.maxTravelTime_ = v; }),\n       \"sets maximum travel time of the queries\")  //\n      (\"max_matching_distance\",\n       po::value(&p.maxMatchingDistance_)\n           ->default_value(p.maxMatchingDistance_),\n       \"sets the maximum matching distance of the queries\")  //\n      (\"fastest_direct_factor\",\n       po::value(&p.fastestDirectFactor_)\n           ->default_value(p.fastestDirectFactor_),\n       \"sets fastest direct factor of the queries\")  //\n      (\"lb_rank\", po::value(&lb_rank)->default_value(lb_rank),\n       \"emit queries uniformly distributed over the lower bounds (lb) ranks, \"\n       \"lb rank n:  2^n-th stop when sorting all stops by their lb value from \"\n       \"the start (min. rank: 4, max. rank: derived from number of eligible \"\n       \"stops)\");\n  add_data_path_opt(desc, data_path);\n  auto vm = parse_opt(ac, av, desc);\n\n  if (vm.count(\"help\")) {\n    std::cout << desc << \"\\n\";\n    return 0;\n  }\n\n  auto const c = config::read(data_path / \"config.yml\");\n  utl::verify(c.timetable_.has_value(), \"timetable required\");\n  utl::verify(!modes || c.use_street_routing(),\n              \"intermodal requires street routing\");\n\n  auto d = data{data_path, c};\n  utl::verify(d.tt_, \"timetable required\");\n\n  first_day = first_day\n                  ? d.tt_->date_range_.clamp(*first_day)\n                  : std::chrono::time_point_cast<date::sys_days::duration>(\n                        d.tt_->external_interval().from_);\n  last_day = last_day ? d.tt_->date_range_.clamp(\n                            std::max(*first_day + date::days{1U}, *last_day))\n                      : d.tt_->date_range_.clamp(*first_day + date::days{14U});\n  if (*first_day == *last_day) {\n    fmt::println(\n        \"can not generate queries: date range [{}, {}] has zero length after \"\n        \"clamping\",\n        *first_day, *last_day);\n    return 1;\n  }\n  fmt::println(\"date range: [{}, {}], tt={}\", *first_day, *last_day,\n               d.tt_->external_interval());\n\n  auto const use_odm_bounds = modes && use_odm && d.odm_bounds_ != nullptr;\n  auto node_rtree = point_rtree<osr::node_idx_t>{};\n  if (modes) {\n    if (modes->empty()) {\n      fmt::println(\n          \"can not generate queries: provided modes option without valid \"\n          \"mode\");\n      return 1;\n    }\n    std::cout << \"modes:\";\n    for (auto const m : *modes) {\n      std::cout << \" \" << m;\n    }\n    std::cout << \"\\n\";\n\n    p.directModes_ = *modes;\n    p.preTransitModes_ = *modes;\n    p.postTransitModes_ = *modes;\n\n    auto const mode_match = [&](auto const node) {\n      auto const can_walk = [&](auto const x) {\n        return utl::any_of(d.w_->r_->node_ways_[x], [&](auto const w) {\n          return d.w_->r_->way_properties_[w].is_foot_accessible();\n        });\n      };\n\n      auto const can_bike = [&](auto const x) {\n        return utl::any_of(d.w_->r_->node_ways_[x], [&](auto const w) {\n          return d.w_->r_->way_properties_[w].is_bike_accessible();\n        });\n      };\n\n      auto const can_car = [&](auto const x) {\n        return utl::any_of(d.w_->r_->node_ways_[x], [&](auto const w) {\n          return d.w_->r_->way_properties_[w].is_car_accessible();\n        });\n      };\n\n      return vm.count(\"all\") ? ((!use_walk || can_walk(node)) &&\n                                (!use_bike || can_bike(node)) &&\n                                (!(use_car || use_odm) || can_car(node)))\n                             : ((use_walk && can_walk(node)) ||\n                                (use_bike && can_bike(node)) ||\n                                ((use_car || use_odm) && can_car(node)));\n    };\n\n    auto const in_bounds = [&](auto const& pos) {\n      return !use_odm_bounds || d.odm_bounds_->contains(pos);\n    };\n\n    for (auto i = osr::node_idx_t{0U}; i < d.w_->n_nodes(); ++i) {\n      if (mode_match(i) && in_bounds(d.w_->get_node_pos(i))) {\n        node_rtree.add(d.w_->get_node_pos(i), i);\n      }\n    }\n  } else {\n    fmt::println(\"station-to-station\");\n  }\n\n  auto stops = std::vector<n::location_idx_t>{};\n  for (auto i = 0U; i != d.tt_->n_locations(); ++i) {\n    auto const l = n::location_idx_t{i};\n    if (use_odm_bounds &&\n        !d.odm_bounds_->contains(d.tt_->locations_.coordinates_[l])) {\n      continue;\n    }\n    stops.emplace_back(l);\n  }\n\n  auto ss = std::optional<n::routing::search_state>{};\n  auto rs = std::optional<n::routing::raptor_state>{};\n  if (lb_rank) {\n    ss = n::routing::search_state{};\n    rs = n::routing::raptor_state{};\n    fmt::println(\"from and to pairings by lower bounds rank\");\n  } else {\n    fmt::println(\"from and to uniformly at random\");\n  }\n\n  auto const get_place =\n      [&](n::location_idx_t const l) -> std::optional<std::string> {\n    if (!modes) {\n      return d.tags_->id(*d.tt_, l);\n    }\n\n    auto const nodes =\n        node_rtree.in_radius(d.tt_->locations_.coordinates_[l], max_dist);\n    if (nodes.empty()) {\n      return std::nullopt;\n    }\n\n    auto const pos = d.w_->get_node_pos(rand_in(nodes));\n    return fmt::format(\"{},{}\", pos.lat(), pos.lng());\n  };\n\n  auto const random_from_to = [&](auto const r) {\n    auto from_place = std::optional<std::string>{};\n    auto to_place = std::optional<std::string>{};\n\n    for (auto x = 0U; x != 1000U; ++x) {\n      auto const from_stop = random_stop(*d.tt_, stops);\n      from_place = get_place(from_stop);\n      if (!from_place) {\n        continue;\n      }\n\n      if (lb_rank) {\n        auto const s = n::routing::search<\n            n::direction::kBackward,\n            n::routing::raptor<n::direction::kBackward, false, 0,\n                               n::routing::search_mode::kOneToAll>>{\n            *d.tt_, nullptr, *ss, *rs,\n            nigiri::routing::query{\n                .start_time_ = d.tt_->date_range_.from_,\n                .destination_ = {{from_stop, n::duration_t{0U}, 0}}}};\n        utl::sort(stops, [&](auto const& a, auto const& b) {\n          return ss->travel_time_lower_bound_[to_idx(a)] <\n                 ss->travel_time_lower_bound_[to_idx(b)];\n        });\n        to_place = get_place(stops[r]);\n      } else {\n        to_place = get_place(random_stop(*d.tt_, stops));\n      }\n      if (to_place) {\n        break;\n      }\n    }\n\n    p.fromPlace_ = *from_place;\n    p.toPlace_ = *to_place;\n  };\n\n  auto const random_time = [&]() {\n    using namespace std::chrono_literals;\n    p.time_ =\n        *first_day +\n        rand_in(0U,\n                static_cast<std::uint32_t>((*last_day - *first_day).count())) *\n            date::days{1U} +\n        (time_of_day ? *time_of_day : rand_in(6U, 18U)) * 1h;\n  };\n\n  {\n    auto out = std::ofstream{\"queries.txt\"};\n    auto const progress_tracker =\n        utl::activate_progress_tracker(fmt::format(\"generating {} queries\", n));\n    progress_tracker->in_high(n);\n    auto const silencer = utl::global_progress_bars{false};\n    for (auto [i, r] = std::tuple{0U, kMinRank}; i != n;\n         ++i, r = r * 2U < stops.size() ? r * 2U : kMinRank) {\n      random_from_to(r);\n      random_time();\n      out << p.to_url(\"/api/v1/plan\") << \"\\n\";\n      progress_tracker->increment();\n    }\n  }\n\n  return 0;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "exe/main.cc",
    "content": "#include <cctype>\n#include <filesystem>\n#include <iostream>\n#include <string>\n#include <string_view>\n\n#include \"boost/program_options.hpp\"\n#include \"boost/url/decode_view.hpp\"\n\n#include \"google/protobuf/stubs/common.h\"\n\n#include \"utl/progress_tracker.h\"\n#include \"utl/to_vec.h\"\n\n#include \"nigiri/rt/util.h\"\n\n#include \"motis/analyze_shapes.h\"\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/import.h\"\n#include \"motis/logging.h\"\n#include \"motis/server.h\"\n\n#include \"./flags.h\"\n\n#if defined(USE_MIMALLOC) && defined(_WIN32)\n#include \"mimalloc-new-delete.h\"\n#endif\n\n#if !defined(MOTIS_VERSION)\n#define MOTIS_VERSION \"unknown\"\n#endif\n\nnamespace po = boost::program_options;\nusing namespace std::string_view_literals;\nnamespace fs = std::filesystem;\n\nnamespace motis {\nint generate(int, char**);\nint batch(int, char**);\nint compare(int, char**);\nint extract(int, char**);\nint params(int, char**);\n}  // namespace motis\n\nusing namespace motis;\n\nint main(int ac, char** av) {\n  auto const motis_version = std::string_view{MOTIS_VERSION};\n  if (ac > 1 && av[1] == \"--help\"sv) {\n    fmt::println(\n        \"MOTIS {}\\n\\n\"\n        \"Usage:\\n\"\n        \"  --help    print this help message\\n\"\n        \"  --version print program version\\n\\n\"\n        \"Commands:\\n\"\n        \"  generate   generate random queries and write them to a file\\n\"\n        \"  batch      run queries from a file\\n\"\n        \"  params     update query parameters for a batch file\\n\"\n        \"  compare    compare results from different batch runs\\n\"\n        \"  config     generate a config file from a list of input files\\n\"\n        \"  import     prepare input data, creates the data directory\\n\"\n        \"  server     starts a web server serving the API\\n\"\n        \"  extract    trips from a Itinerary to GTFS timetable\\n\"\n        \"  pb2json    convert GTFS-RT protobuf to JSON\\n\"\n        \"  json2pb    convert JSON to GTFS-RT protobuf\\n\"\n        \"  shapes     print shape segmentation for trips\\n\",\n        motis_version);\n    return 0;\n  } else if (ac <= 1 || (ac >= 2 && av[1] == \"--version\"sv)) {\n    fmt::println(\"{}\", motis_version);\n    return 0;\n  }\n\n  // Skip program argument, quit if no command.\n  --ac;\n  ++av;\n\n  auto return_value = 0;\n\n  // Execute command.\n  auto const cmd = std::string_view{av[0]};\n  switch (cista::hash(cmd)) {\n    case cista::hash(\"extract\"): return_value = extract(ac, av); break;\n    case cista::hash(\"generate\"): return_value = generate(ac, av); break;\n    case cista::hash(\"params\"): return_value = params(ac, av); break;\n    case cista::hash(\"batch\"): return_value = batch(ac, av); break;\n    case cista::hash(\"compare\"): return_value = compare(ac, av); break;\n\n    case cista::hash(\"config\"): {\n      auto paths = std::vector<std::string>{};\n      for (auto i = 1; i != ac; ++i) {\n        paths.push_back(std::string{av[i]});\n      }\n      if (paths.empty() || paths.front() == \"--help\") {\n        fmt::println(\n            \"usage: motis config [PATHS...]\\n\\n\"\n            \"Generates a config.yml file in the current working \"\n            \"directory.\\n\\n\"\n            \"File type will be determined based on extension:\\n\"\n            \"  - \\\".osm.pbf\\\" will be used as OpenStreetMap file.\\n\"\n            \"    This enables street routing, geocoding and map tiles\\n\"\n            \"  - the rest will be interpreted as static timetables.\\n\"\n            \"    This enables transit routing.\"\n            \"\\n\\n\"\n            \"Example: motis config germany-latest.osm.pbf \"\n            \"germany.gtfs.zip\\n\");\n        return_value = paths.empty() ? 1 : 0;\n        break;\n      }\n      std::ofstream{\"config.yml\"} << config::read_simple(paths) << \"\\n\";\n      return_value = 0;\n      break;\n    }\n\n    case cista::hash(\"server\"):\n      try {\n        auto data_path = fs::path{\"data\"};\n        auto log_lvl = std::string{};\n\n        auto desc = po::options_description{\"Server Options\"};\n        add_data_path_opt(desc, data_path);\n        add_log_level_opt(desc, log_lvl);\n        add_help_opt(desc);\n        auto vm = parse_opt(ac, av, desc);\n        if (vm.count(\"help\")) {\n          std::cout << desc << \"\\n\";\n          return_value = 0;\n          break;\n        }\n\n        auto const c = config::read(data_path / \"config.yml\");\n        if ((return_value = set_log_level(c))) {\n          break;\n        }\n        if (vm.count(\"log-level\") &&\n            (return_value = set_log_level(std::move(log_lvl)))) {\n          break;\n        }\n        return_value = server(data{data_path, c}, c, motis_version);\n      } catch (std::exception const& e) {\n        std::cerr << \"unable to start server: \" << e.what() << \"\\n\";\n        return_value = 1;\n      }\n      break;\n\n    case cista::hash(\"import\"): {\n      auto c = config{};\n      try {\n        auto data_path = fs::path{\"data\"};\n        auto config_path = fs::path{\"config.yml\"};\n        auto filter_tasks = std::vector<std::string>{};\n\n        auto desc = po::options_description{\"Import Options\"};\n        add_data_path_opt(desc, data_path);\n        add_config_path_opt(desc, config_path);\n        add_help_opt(desc);\n        desc.add_options()  //\n            (\"filter\",\n             boost::program_options::value<std::vector<std::string>>(\n                 &filter_tasks)\n                 ->composing(),\n             \"Filter tasks and only run selected import tasks. Tasks have to \"\n             \"be active based on the configuration. Available tasks are:\\n\"\n             \"  - osr (street_routing)\\n\"\n             \"  - adr (geocoding/reverse_geocoding)\\n\"\n             \"  - tt (timetable)\\n\"\n             \"  - tbd (timetable.tb)\\n\"\n             \"  - adr_extend (timetable+geocoding)\\n\"\n             \"  - osr_footpath\\n\"\n             \"  - matches (timetable+street_routing)\\n\"\n             \"  - route_shapes (timetable.route_shapes)\\n\"\n             \"  - tiles\\n\");\n        auto vm = parse_opt(ac, av, desc);\n        if (vm.count(\"help\")) {\n          std::cout << desc << \"\\n\";\n          return_value = 0;\n          break;\n        }\n\n        c = config::read(config_path);\n        if ((return_value = set_log_level(c))) {\n          break;\n        }\n        auto const bars = utl::global_progress_bars{false};\n        import(\n            c, std::move(data_path),\n            filter_tasks.empty() ? std::nullopt : std::optional{filter_tasks});\n        return_value = 0;\n      } catch (std::exception const& e) {\n        fmt::println(\"unable to import: {}\", e.what());\n        fmt::println(\"config:\\n{}\", fmt::streamed(c));\n        return_value = 1;\n      }\n      break;\n    }\n\n    case cista::hash(\"pb2json\"): {\n      try {\n        auto p = fs::path{};\n\n        auto desc = po::options_description{\"GTFS-RT Protobuf to JSON\"};\n        desc.add_options()  //\n            (\"path,p\", boost::program_options::value(&p)->default_value(p),\n             \"Path to Protobuf GTFS-RT file\");\n        auto vm = parse_opt(ac, av, desc);\n        if (vm.count(\"help\")) {\n          std::cout << desc << \"\\n\";\n          return_value = 0;\n          break;\n        }\n\n        auto const protobuf = cista::mmap{p.generic_string().c_str(),\n                                          cista::mmap::protection::READ};\n        fmt::println(\"{}\", nigiri::rt::protobuf_to_json(protobuf.view()));\n        return_value = 0;\n      } catch (std::exception const& e) {\n        fmt::println(\"error: \", e.what());\n        return_value = 1;\n      }\n      break;\n    }\n\n    case cista::hash(\"json2pb\"): {\n      try {\n        auto p = fs::path{};\n\n        auto desc = po::options_description{\"GTFS-RT JSON to Protobuf\"};\n        desc.add_options()  //\n            (\"path,p\", boost::program_options::value(&p)->default_value(p),\n             \"Path to GTFS-RT JSON file\");\n        auto vm = parse_opt(ac, av, desc);\n        if (vm.count(\"help\")) {\n          std::cout << desc << \"\\n\";\n          return_value = 0;\n          break;\n        }\n\n        auto const protobuf = cista::mmap{p.generic_string().c_str(),\n                                          cista::mmap::protection::READ};\n        fmt::println(\"{}\", nigiri::rt::json_to_protobuf(protobuf.view()));\n        return_value = 0;\n      } catch (std::exception const& e) {\n        fmt::println(\"error: \", e.what());\n        return_value = 1;\n      }\n      break;\n    }\n\n    case cista::hash(\"shapes\"): {\n      try {\n        auto data_path = fs::path{\"data\"};\n\n        auto desc = po::options_description{\"Analyze Shapes Options\"};\n        add_trip_id_opt(desc);\n        add_data_path_opt(desc, data_path);\n        add_help_opt(desc);\n\n        auto vm = parse_opt(ac, av, desc);\n        if (vm.count(\"help\")) {\n          std::cout << desc << \"\\n\";\n          return_value = 0;\n          break;\n        }\n\n        if (vm.count(\"trip-id\") == 0) {\n          std::cerr << \"missing trip-ids\\n\";\n          return_value = 2;\n          break;\n        }\n        auto const c = config::read(data_path / \"config.yml\");\n        auto const ids = utl::to_vec(\n            vm[\"trip-id\"].as<std::vector<std::string>>(),\n            [](auto const& trip_id) {\n              // Set space_as_plus = true\n              auto const opts = boost::urls::encoding_opts{true};\n              auto const decoded = boost::urls::decode_view{trip_id, opts};\n              return std::string{decoded.begin(), decoded.end()};\n            });\n\n        return_value = analyze_shapes(data{data_path, c}, ids) ? 0 : 1;\n      } catch (std::exception const& e) {\n        std::cerr << \"unable to analyse shapes: \" << e.what() << \"\\n\";\n        return_value = 1;\n      }\n      break;\n    }\n\n    default:\n      fmt::println(\n          \"Invalid command. Type motis --help for a list of commands.\");\n      return_value = 1;\n      break;\n  }\n\n  google::protobuf::ShutdownProtobufLibrary();\n  return return_value;\n}\n"
  },
  {
    "path": "exe/params.cc",
    "content": "#include <fstream>\n#include <iostream>\n\n#include \"boost/url/url.hpp\"\n\n#include \"utl/file_utils.h\"\n#include \"utl/parser/cstr.h\"\n\n#include \"./flags.h\"\n\nnamespace po = boost::program_options;\n\nnamespace motis {\n\nint params(int ac, char** av) {\n  auto params = std::string{};\n  auto in = std::string{};\n  auto out = std::string{};\n\n  auto desc = po::options_description{\"Options\"};\n  desc.add_options()  //\n      (\"help\", \"Prints this help message\")  //\n      (\"params,p\", po::value(&params)->default_value(params))  //\n      (\"in,i\", po::value(&in)->default_value(in))  //\n      (\"out,o\", po::value(&out)->default_value(out));\n\n  auto vm = parse_opt(ac, av, desc);\n  if (vm.count(\"help\")) {\n    std::cout << desc << \"\\n\";\n    return 0;\n  }\n\n  auto const override = boost::urls::url{params};\n\n  auto out_file = std::ofstream{out};\n  auto in_file = utl::open_file(in);\n  auto line = std::optional<std::string>{};\n  while ((line = utl::read_line(in_file))) {\n    auto query = boost::urls::url{*line};\n    for (auto const& x : override.params()) {\n      query.params().set(x.key, x.value);\n    }\n    out_file << query << \"\\n\";\n  }\n\n  return 0U;\n}\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/adr_extend_tt.h",
    "content": "#pragma once\n\n#include \"date/tz.h\"\n\n#include \"nigiri/routing/clasz_mask.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\n// Starts counting timetable places at the last OSM place.\nusing adr_extra_place_idx_t =\n    cista::strong<std::uint32_t, struct adr_extra_place_idx_>;\n\nusing tz_map_t = vector_map<adr_extra_place_idx_t, date::time_zone const*>;\n\nstruct adr_ext {\n  vector_map<nigiri::location_idx_t, adr_extra_place_idx_t> location_place_;\n  vector_map<adr_extra_place_idx_t, nigiri::routing::clasz_mask_t> place_clasz_;\n  vector_map<adr_extra_place_idx_t, float> place_importance_;\n};\n\ndate::time_zone const* get_tz(nigiri::timetable const&,\n                              adr_ext const*,\n                              tz_map_t const*,\n                              nigiri::location_idx_t);\n\nadr_ext adr_extend_tt(nigiri::timetable const&,\n                      adr::area_database const*,\n                      adr::typeahead&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/analyze_shapes.h",
    "content": "#include <string>\n#include <vector>\n\n#include \"motis/data.h\"\n\nnamespace motis {\n\nbool analyze_shapes(data const&, std::vector<std::string> const& trip_ids);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/box_rtree.h",
    "content": "#pragma once\n\n#include <algorithm>\n#include <array>\n\n#include \"cista/strong.h\"\n\n#include \"rtree.h\"\n\n#include \"geo/box.h\"\n#include \"geo/latlng.h\"\n\nnamespace motis {\n\ntemplate <typename T, typename Fn>\nconcept BoxRtreePosHandler = requires(geo::box const& b, T const x, Fn&& f) {\n  { std::forward<Fn>(f)(b, x) };\n};\n\ntemplate <typename T>\nstruct box_rtree {\n  box_rtree() : rtree_{rtree_new()} {}\n\n  ~box_rtree() {\n    if (rtree_ != nullptr) {\n      rtree_free(rtree_);\n    }\n  }\n\n  box_rtree(box_rtree const& o) {\n    if (this != &o) {\n      if (rtree_ != nullptr) {\n        rtree_free(rtree_);\n      }\n      rtree_ = rtree_clone(o.rtree_);\n    }\n  }\n\n  box_rtree(box_rtree&& o) {\n    if (this != &o) {\n      rtree_ = o.rtree_;\n      o.rtree_ = nullptr;\n    }\n  }\n\n  box_rtree& operator=(box_rtree const& o) {\n    if (this != &o) {\n      if (rtree_ != nullptr) {\n        rtree_free(rtree_);\n      }\n      rtree_ = rtree_clone(o.rtree_);\n    }\n    return *this;\n  }\n\n  box_rtree& operator=(box_rtree&& o) {\n    if (this != &o) {\n      rtree_ = o.rtree_;\n      o.rtree_ = nullptr;\n    }\n    return *this;\n  }\n\n  void add(geo::box const& b, T const t) {\n    auto const min_corner = b.min_.lnglat();\n    auto const max_corner = b.max_.lnglat();\n    rtree_insert(\n        rtree_, min_corner.data(), max_corner.data(),\n        reinterpret_cast<void*>(static_cast<std::size_t>(cista::to_idx(t))));\n  }\n\n  void remove(geo::box const& b, T const t) {\n    auto const min_corner = b.min_.lnglat();\n    auto const max_corner = b.max_.lnglat();\n    rtree_delete(\n        rtree_, min_corner.data(), max_corner.data(),\n        reinterpret_cast<void*>(static_cast<std::size_t>(cista::to_idx(t))));\n  }\n\n  std::vector<T> in_radius(geo::latlng const& x, double distance) const {\n    auto ret = std::vector<T>{};\n    in_radius(x, distance, [&](auto&& item) { ret.emplace_back(item); });\n    return ret;\n  }\n\n  template <typename Fn>\n  void in_radius(geo::latlng const& x, double distance, Fn&& fn) const {\n    auto const rad_sq = distance * distance;\n    auto const approx_distance_lng_degrees =\n        geo::approx_distance_lng_degrees(x);\n    find(geo::box{x, distance}, [&](geo::box const& box, T const item) {\n      auto const closest =\n          geo::latlng{std::clamp(x.lat(), box.min_.lat(), box.max_.lat()),\n                      std::clamp(x.lng(), box.min_.lng(), box.max_.lng())};\n      if (geo::approx_squared_distance(x, closest,\n                                       approx_distance_lng_degrees) < rad_sq) {\n        fn(item);\n      }\n    });\n  }\n\n  template <typename Fn>\n  void find(geo::box const& b, Fn&& fn) const {\n    auto const min = b.min_.lnglat();\n    auto const max = b.max_.lnglat();\n    rtree_search(\n        rtree_, min.data(), max.data(),\n        [](double const* min_corner, double const* max_corner, void const* item,\n           void* udata) {\n          if constexpr (BoxRtreePosHandler<T, Fn>) {\n            (*reinterpret_cast<Fn*>(udata))(\n                geo::box{geo::latlng{min_corner[1], min_corner[0]},\n                         geo::latlng{max_corner[1], max_corner[0]}},\n                T{static_cast<cista::base_t<T>>(\n                    reinterpret_cast<std::size_t>(item))});\n          } else {\n            (*reinterpret_cast<Fn*>(udata))(T{static_cast<cista::base_t<T>>(\n                reinterpret_cast<std::size_t>(item))});\n          }\n          return true;\n        },\n        &fn);\n  }\n\n  template <typename Fn>\n  void find(geo::latlng const& pos, Fn&& fn) const {\n    return find(geo::box{pos, pos}, std::forward<Fn>(fn));\n  }\n\n  rtree* rtree_{nullptr};\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/clog_redirect.h",
    "content": "#pragma once\n\n#include <fstream>\n#include <memory>\n#include <mutex>\n#include <streambuf>\n\nnamespace motis {\n\nstruct clog_redirect {\n  explicit clog_redirect(char const* log_file_path);\n\n  clog_redirect(clog_redirect const&) = delete;\n  clog_redirect(clog_redirect&&) = delete;\n  clog_redirect& operator=(clog_redirect const&) = delete;\n  clog_redirect& operator=(clog_redirect&&) = delete;\n\n  ~clog_redirect();\n\n  static void set_enabled(bool);\n\nprivate:\n  std::ofstream sink_;\n  std::unique_ptr<std::streambuf> sink_buf_;\n  std::streambuf* backup_clog_{};\n  bool active_{};\n  std::mutex mutex_;\n\n  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)\n  static bool enabled_;\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/compute_footpaths.h",
    "content": "#pragma once\n\n#include \"cista/memory_holder.h\"\n\n#include \"osr/routing/profile.h\"\n#include \"osr/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\nusing elevator_footpath_map_t = hash_map<\n    osr::node_idx_t,\n    hash_set<std::pair<nigiri::location_idx_t, nigiri::location_idx_t>>>;\n\nstruct routed_transfers_settings {\n  osr::search_profile profile_;\n  nigiri::profile_idx_t profile_idx_;\n  double max_matching_distance_;\n  bool extend_missing_{false};\n  std::chrono::seconds max_duration_;\n  std::function<bool(nigiri::location_idx_t)> is_candidate_{};\n};\n\nelevator_footpath_map_t compute_footpaths(\n    osr::ways const&,\n    osr::lookup const&,\n    osr::platforms const&,\n    nigiri::timetable&,\n    osr::elevation_storage const*,\n    bool update_coordinates,\n    std::vector<routed_transfers_settings> const& settings);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/config.h",
    "content": "#pragma once\n\n#include <filesystem>\n#include <functional>\n#include <iosfwd>\n#include <map>\n#include <optional>\n#include <set>\n#include <thread>\n#include <variant>\n#include <vector>\n\n#include \"cista/hashing.h\"\n\n#include \"utl/verify.h\"\n\nnamespace motis {\n\nusing headers_t = std::map<std::string, std::string>;\n\nstruct config {\n  friend std::ostream& operator<<(std::ostream&, config const&);\n  static config read_simple(std::vector<std::string> const& args);\n  static config read(std::filesystem::path const&);\n  static config read(std::string const&);\n\n  void verify() const;\n  void verify_input_files_exist() const;\n\n  bool requires_rt_timetable_updates() const;\n  bool shapes_debug_api_enabled() const;\n  bool has_gbfs_feeds() const;\n  bool has_prima() const;\n  bool has_elevators() const;\n  bool use_street_routing() const;\n\n  bool operator==(config const&) const = default;\n\n  struct server {\n    bool operator==(server const&) const = default;\n    std::string host_{\"0.0.0.0\"};\n    std::string port_{\"8080\"};\n    std::string web_folder_{\"ui\"};\n    unsigned n_threads_{0U};\n    std::optional<std::string> data_attribution_link_{};\n    std::optional<std::vector<std::string>> lbs_{};\n  };\n  std::optional<server> server_{};\n\n  std::optional<std::filesystem::path> osm_{};\n\n  struct tiles {\n    bool operator==(tiles const&) const = default;\n    std::filesystem::path profile_;\n    std::optional<std::filesystem::path> coastline_{};\n    std::size_t db_size_{sizeof(void*) >= 8\n                             ? 256ULL * 1024ULL * 1024ULL * 1024ULL\n                             : 256U * 1024U * 1024U};\n    std::size_t flush_threshold_{100'000};\n  };\n  std::optional<tiles> tiles_{};\n\n  struct timetable {\n    struct dataset {\n      struct rt {\n        bool operator==(rt const&) const = default;\n        cista::hash_t hash() const noexcept {\n          return cista::build_hash(url_, headers_);\n        }\n        std::string url_;\n        std::optional<headers_t> headers_{};\n\n        enum struct protocol { gtfsrt, auser, siri, siri_json };\n        protocol protocol_{protocol::gtfsrt};\n      };\n\n      bool operator==(dataset const&) const = default;\n\n      std::string path_;\n      std::optional<std::string> script_{};\n      bool default_bikes_allowed_{false};\n      bool default_cars_allowed_{false};\n      bool extend_calendar_{false};\n      std::optional<std::map<std::string, bool>> clasz_bikes_allowed_{};\n      std::optional<std::map<std::string, bool>> clasz_cars_allowed_{};\n      std::optional<std::vector<rt>> rt_{};\n      std::optional<std::string> default_timezone_{};\n    };\n\n    struct shapes_debug {\n      bool operator==(shapes_debug const&) const = default;\n      std::filesystem::path path_;\n      std::optional<std::vector<std::string>> trips_{};\n      std::optional<std::vector<std::string>> route_ids_{};\n      std::optional<std::vector<unsigned>> route_indices_{};\n      bool all_{false};\n      bool all_with_beelines_{false};\n      unsigned slow_{0U};\n    };\n\n    struct route_shapes {\n      enum class mode { all, missing };\n\n      bool operator==(route_shapes const&) const = default;\n      mode mode_{mode::all};\n      bool cache_reuse_old_osm_data_{false};\n      std::size_t cache_db_size_{sizeof(void*) >= 8\n                                     ? 256ULL * 1024ULL * 1024ULL * 1024ULL\n                                     : 256U * 1024U * 1024U};\n      std::optional<std::map<std::string, bool>> clasz_{};\n      unsigned max_stops_{0U};\n      unsigned n_threads_{0U};\n      bool debug_api_{false};\n      std::optional<shapes_debug> debug_{};\n    };\n\n    bool operator==(timetable const&) const = default;\n\n    std::string first_day_{\"TODAY\"};\n    std::uint16_t num_days_{365U};\n    bool tb_{false};\n    bool railviz_{true};\n    bool with_shapes_{true};\n    bool adjust_footpaths_{true};\n    bool merge_dupes_intra_src_{false};\n    bool merge_dupes_inter_src_{false};\n    unsigned link_stop_distance_{100U};\n    unsigned update_interval_{60};\n    unsigned http_timeout_{30};\n    bool canned_rt_{false};\n    bool incremental_rt_update_{false};\n    bool use_osm_stop_coordinates_{false};\n    bool extend_missing_footpaths_{false};\n    std::uint16_t max_footpath_length_{15};\n    double max_matching_distance_{25.0};\n    double preprocess_max_matching_distance_{250.0};\n    std::optional<std::string> default_timezone_{};\n    std::map<std::string, dataset> datasets_{};\n    std::optional<std::filesystem::path> assistance_times_{};\n    std::optional<route_shapes> route_shapes_{};\n  };\n  std::optional<timetable> timetable_{};\n\n  struct gbfs {\n    bool operator==(gbfs const&) const = default;\n\n    struct ttl {\n      bool operator==(ttl const&) const = default;\n\n      std::optional<std::map<std::string, unsigned>> default_{};\n      std::optional<std::map<std::string, unsigned>> overwrite_{};\n    };\n\n    struct restrictions {\n      bool operator==(restrictions const&) const = default;\n      bool ride_start_allowed_{true};\n      bool ride_end_allowed_{true};\n      bool ride_through_allowed_{true};\n      std::optional<bool> station_parking_{};\n      std::optional<std::string> return_constraint_{};\n    };\n\n    struct oauth_settings {\n      bool operator==(oauth_settings const&) const = default;\n      std::string token_url_;\n      std::string client_id_;\n      std::string client_secret_;\n      std::optional<headers_t> headers_{};\n      std::optional<unsigned> expires_in_;\n    };\n\n    struct feed {\n      bool operator==(feed const&) const = default;\n      std::string url_;\n      std::optional<headers_t> headers_{};\n      std::optional<oauth_settings> oauth_{};\n      std::optional<\n          std::variant<std::string, std::map<std::string, std::string>>>\n          group_{};\n      std::optional<\n          std::variant<std::string, std::map<std::string, std::string>>>\n          color_{};\n      std::optional<ttl> ttl_{};\n    };\n\n    struct group {\n      bool operator==(group const&) const = default;\n      std::optional<std::string> name_{};\n      std::optional<std::string> color_{};\n      std::optional<std::string> url_{};\n    };\n\n    std::map<std::string, feed> feeds_{};\n    std::map<std::string, group> groups_{};\n    std::map<std::string, restrictions> default_restrictions_{};\n    unsigned update_interval_{60};\n    unsigned http_timeout_{30};\n    unsigned cache_size_{50};\n    std::optional<std::string> proxy_{};\n    std::optional<ttl> ttl_{};\n  };\n  std::optional<gbfs> gbfs_{};\n\n  struct prima {\n    bool operator==(prima const&) const = default;\n    std::string url_{};\n    std::optional<std::string> bounds_{};\n    std::optional<std::string> ride_sharing_bounds_{};\n  };\n  std::optional<prima> prima_{};\n\n  struct elevators {\n    bool operator==(elevators const&) const = default;\n    std::optional<std::string> url_{};\n    std::optional<std::string> init_{};\n    std::optional<std::string> osm_mapping_{};\n    unsigned http_timeout_{10};\n    std::optional<headers_t> headers_{};\n  };\n\n  unsigned n_threads() const;\n\n  std::optional<elevators> const& get_elevators() const;\n\n  std::variant<bool, std::optional<elevators>> elevators_{false};\n\n  struct street_routing {\n    bool operator==(street_routing const&) const = default;\n    std::optional<std::filesystem::path> elevation_data_dir_;\n  };\n\n  std::optional<street_routing> get_street_routing() const;\n\n  std::variant<bool, std::optional<street_routing>> street_routing_{false};\n\n  struct limits {\n    bool operator==(limits const&) const = default;\n    unsigned stoptimes_max_results_{256U};\n    unsigned plan_max_results_{256U};\n    unsigned plan_max_search_window_minutes_{5760U};\n    unsigned stops_max_results_{2048U};\n    unsigned onetomany_max_many_{128U};\n    unsigned onetoall_max_results_{65535U};\n    unsigned onetoall_max_travel_minutes_{90U};\n    unsigned routing_max_timeout_seconds_{90U};\n    unsigned gtfsrt_expose_max_trip_updates_{100U};\n    unsigned street_routing_max_prepost_transit_seconds_{3600U};\n    unsigned street_routing_max_direct_seconds_{21600U};\n    unsigned geocode_max_suggestions_{10U};\n    unsigned reverse_geocode_max_results_{5U};\n  };\n  limits get_limits() const { return limits_.value_or(limits{}); }\n  std::optional<limits> limits_{};\n\n  struct logging {\n    bool operator==(logging const&) const = default;\n    std::optional<std::string> log_level_{};\n  };\n  std::optional<logging> logging_{};\n\n  bool osr_footpath_{false};\n  bool geocoding_{false};\n  bool reverse_geocoding_{false};\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/constants.h",
    "content": "#pragma once\n\nnamespace motis {\n\n// search radius for neighbors to route to [meters]\nconstexpr auto const kMaxDistance = 2000;\n\n// max distance from start/destination coordinate to way segment [meters]\nconstexpr auto const kMaxMatchingDistance = 25.0;\n\nconstexpr auto const kMaxWheelchairMatchingDistance = 8.0;\n\n// max distance from gbfs vehicle/station to way segment [meters]\nconstexpr auto const kMaxGbfsMatchingDistance = 100.0;\n\n// distance between location in timetable and OSM platform coordinate [meters]\nconstexpr auto const kMaxAdjust = 200;\n\n// multiplier for transfer times\nconstexpr auto const kTransferTimeMultiplier = 1.5F;\n\n// footpaths of public transport locations around this distance\n// are updated on elevator status changes [meters]\nconstexpr auto const kElevatorUpdateRadius = 1000.;\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/ctx_data.h",
    "content": "#pragma once\n\n#include \"ctx/op_id.h\"\n#include \"ctx/operation.h\"\n\nnamespace motis {\n\nstruct ctx_data {\n  void transition(ctx::transition, ctx::op_id, ctx::op_id) {}\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/ctx_exec.h",
    "content": "#pragma once\n\n#include <iostream>\n\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/asio/post.hpp\"\n\n#include \"ctx/scheduler.h\"\n\n#include \"motis/ctx_data.h\"\n\nnamespace motis {\n\nstruct ctx_exec {\n  ctx_exec(boost::asio::io_context& io, ctx::scheduler<ctx_data>& sched)\n      : io_{io}, sched_{sched} {}\n\n  void exec(auto&& f, net::web_server::http_res_cb_t cb) {\n    sched_.post_void_io(\n        ctx_data{},\n        [&, f = std::move(f), cb = std::move(cb)]() mutable {\n          try {\n            auto res = std::make_shared<net::web_server::http_res_t>(f());\n            boost::asio::post(\n                io_, [cb = std::move(cb), res = std::move(res)]() mutable {\n                  cb(std::move(*res));\n                });\n          } catch (...) {\n            std::cerr << \"UNEXPECTED EXCEPTION\\n\";\n\n            auto str = net::web_server::string_res_t{\n                boost::beast::http::status::internal_server_error, 11};\n            str.body() = \"error\";\n            str.prepare_payload();\n\n            auto res = std::make_shared<net::web_server::http_res_t>(str);\n            boost::asio::post(\n                io_, [cb = std::move(cb), res = std::move(res)]() mutable {\n                  cb(std::move(*res));\n                });\n          }\n        },\n        CTX_LOCATION);\n  }\n\n  boost::asio::io_context& io_;\n  ctx::scheduler<ctx_data>& sched_;\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/data.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"cista/memory_holder.h\"\n\n#include \"date/date.h\"\n\n#include \"nigiri/rt/vdv_aus.h\"\n#include \"nigiri/types.h\"\n\n#include \"osr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/adr_extend_tt.h\"\n#include \"motis/config.h\"\n#include \"motis/elevators/parse_elevator_id_osm_mapping.h\"\n#include \"motis/fwd.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/rt/auser.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\nstruct elevators;\n\ntemplate <typename T>\nstruct point_rtree;\n\ntemplate <typename T>\nusing ptr = std::unique_ptr<T>;\n\nstruct rt {\n  rt();\n  rt(ptr<nigiri::rt_timetable>&&, ptr<elevators>&&, ptr<railviz_rt_index>&&);\n  ~rt();\n  ptr<nigiri::rt_timetable> rtt_;\n  ptr<railviz_rt_index> railviz_rt_;\n  ptr<elevators> e_;\n};\n\nstruct data {\n  data(std::filesystem::path);\n  data(std::filesystem::path, config const&);\n  ~data();\n\n  data(data const&) = delete;\n  data& operator=(data const&) = delete;\n\n  data(data&&);\n  data& operator=(data&&);\n\n  friend std::ostream& operator<<(std::ostream&, data const&);\n\n  void load_osr();\n  void load_tt(std::filesystem::path const&);\n  void load_flex_areas();\n  void load_shapes();\n  void load_railviz();\n  void load_tbd();\n  void load_geocoder();\n  void load_matches();\n  void load_way_matches();\n  void load_reverse_geocoder();\n  void load_tiles();\n  void load_auser_updater(std::string_view, config::timetable::dataset const&);\n\n  void init_rtt(date::sys_days = std::chrono::time_point_cast<date::days>(\n                    std::chrono::system_clock::now()));\n\n  auto cista_members() {\n    // !!! Remember to add all new members !!!\n    return std::tie(config_, motis_version_, initial_response_, t_, adr_ext_,\n                    f_, tz_, r_, tc_, w_, pl_, l_, elevations_, tt_, tbd_,\n                    tags_, location_rtree_, elevator_nodes_,\n                    elevator_osm_mapping_, shapes_, railviz_static_, matches_,\n                    way_matches_, rt_, gbfs_, odm_bounds_, ride_sharing_bounds_,\n                    flex_areas_, metrics_, auser_);\n  }\n\n  std::filesystem::path path_;\n  config config_;\n  std::string_view motis_version_;\n\n  api::initial_response initial_response_;\n  cista::wrapped<adr::typeahead> t_;\n  cista::wrapped<adr_ext> adr_ext_;\n  ptr<adr::formatter> f_;\n  ptr<vector_map<adr_extra_place_idx_t, date::time_zone const*>> tz_;\n  ptr<adr::reverse> r_;\n  ptr<adr::cache> tc_;\n  ptr<osr::ways> w_;\n  ptr<osr::platforms> pl_;\n  ptr<osr::lookup> l_;\n  ptr<osr::elevation_storage> elevations_;\n  cista::wrapped<nigiri::timetable> tt_;\n  cista::wrapped<nigiri::routing::tb::tb_data> tbd_;\n  cista::wrapped<tag_lookup> tags_;\n  ptr<point_rtree<nigiri::location_idx_t>> location_rtree_;\n  ptr<hash_set<osr::node_idx_t>> elevator_nodes_;\n  ptr<elevator_id_osm_mapping_t> elevator_osm_mapping_;\n  ptr<nigiri::shapes_storage> shapes_;\n  ptr<railviz_static_index> railviz_static_;\n  cista::wrapped<vector_map<nigiri::location_idx_t, osr::platform_idx_t>>\n      matches_;\n  ptr<way_matches_storage> way_matches_;\n  ptr<tiles_data> tiles_;\n  std::shared_ptr<rt> rt_{std::make_shared<rt>()};\n  std::shared_ptr<gbfs::gbfs_data> gbfs_{};\n  ptr<odm::bounds> odm_bounds_;\n  ptr<odm::ride_sharing_bounds> ride_sharing_bounds_;\n  ptr<flex::flex_areas> flex_areas_;\n  ptr<metrics_registry> metrics_;\n  ptr<std::map<std::string, auser>> auser_;\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/direct_filter.h",
    "content": "#pragma once\n\n#include <vector>\n\n#include \"nigiri/routing/journey.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis {\n\nvoid direct_filter(std::vector<api::Itinerary> const& direct,\n                   std::vector<nigiri::routing::journey>&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/elevators/elevators.h",
    "content": "#pragma once\n\n#include \"motis/elevators/match_elevator.h\"\n#include \"motis/elevators/parse_elevator_id_osm_mapping.h\"\n#include \"motis/fwd.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis {\n\nstruct elevators {\n  elevators(osr::ways const&,\n            elevator_id_osm_mapping_t const*,\n            hash_set<osr::node_idx_t> const&,\n            vector_map<elevator_idx_t, elevator>&&);\n\n  vector_map<elevator_idx_t, elevator> elevators_;\n  point_rtree<elevator_idx_t> elevators_rtree_;\n  osr::bitvec<osr::node_idx_t> blocked_;\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/elevators/get_state_changes.h",
    "content": "#pragma once\n\n#include <ranges>\n#include <vector>\n\n#include \"fmt/ranges.h\"\n\n#include \"nigiri/common/interval.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/generator.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/verify.h\"\n\nnamespace motis {\n\ntemplate <typename Time>\nstruct state_change {\n  friend bool operator==(state_change const&, state_change const&) = default;\n  Time valid_from_;\n  bool state_;\n};\n\ntemplate <typename Time, bool Default = true>\nstd::vector<state_change<Time>> intervals_to_state_changes(\n    std::vector<nigiri::interval<Time>> const& iv, bool const status) {\n  using Duration = typename Time::duration;\n  auto ret = std::vector<state_change<Time>>{};\n  if (iv.empty()) {\n    ret.push_back({Time{Duration{0}}, status});\n  } else {\n    ret.push_back({Time{Duration{0}}, Default});\n    for (auto const& i : iv) {\n      ret.push_back({i.from_, !Default});\n      ret.push_back({i.to_, Default});\n    }\n  }\n  return ret;\n}\n\ntemplate <typename Time>\nstatic utl::generator<std::pair<Time, std::vector<bool>>> get_state_changes(\n    std::vector<std::vector<state_change<Time>>> const& c) {\n  using It = std::vector<state_change<Time>>::const_iterator;\n\n  struct range {\n    bool is_finished() const { return curr_ == end_; }\n    bool state_{false};\n    It curr_, end_;\n  };\n\n  auto its = utl::to_vec(c, [](auto&& v) {\n    utl::verify(!v.empty(), \"empty state vector not allowed\");\n    return range{v[0].state_, v.begin(), v.end()};\n  });\n\n  auto const all_finished = [&]() {\n    return std::ranges::all_of(its, [&](auto&& r) { return r.is_finished(); });\n  };\n\n  auto const next = [&]() -> range& {\n    auto const it = std::ranges::min_element(its, [&](auto&& a, auto&& b) {\n      if (a.curr_ == a.end_) {\n        return false;\n      } else if (b.curr_ == b.end_) {\n        return true;\n      } else {\n        return a.curr_->valid_from_ < b.curr_->valid_from_;\n      }\n    });\n    assert(it != end(its));\n    return *it;\n  };\n\n  auto const get_state = [&]() -> std::vector<bool> {\n    auto s = std::vector<bool>(its.size());\n    for (auto const [i, r] : utl::enumerate(its)) {\n      s[i] = r.state_;\n    }\n    return s;\n  };\n\n  auto pred_t = std::optional<std::pair<Time, std::vector<bool>>>{};\n  while (!all_finished()) {\n    auto& n = next();\n    auto const t = n.curr_->valid_from_;\n    n.state_ = n.curr_->state_;\n    ++n.curr_;\n\n    auto const state = std::pair{t, get_state()};\n    if (!pred_t.has_value()) {\n      pred_t = state;\n      continue;\n    }\n\n    if (pred_t->first != state.first) {\n      co_yield *pred_t;\n    }\n    pred_t = state;\n  }\n\n  if (pred_t.has_value()) {\n    co_yield *pred_t;\n  }\n\n  co_return;\n}\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/elevators/match_elevator.h",
    "content": "#pragma once\n\n#include \"osr/types.h\"\n\n#include \"motis/elevators/parse_elevator_id_osm_mapping.h\"\n#include \"motis/fwd.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\npoint_rtree<elevator_idx_t> create_elevator_rtree(\n    vector_map<elevator_idx_t, elevator> const&);\n\nosr::hash_set<osr::node_idx_t> get_elevator_nodes(osr::ways const&);\n\nelevator_idx_t match_elevator(point_rtree<elevator_idx_t> const&,\n                              vector_map<elevator_idx_t, elevator> const&,\n                              osr::ways const&,\n                              osr::node_idx_t);\n\nosr::bitvec<osr::node_idx_t> get_blocked_elevators(\n    osr::ways const&,\n    elevator_id_osm_mapping_t const*,\n    vector_map<elevator_idx_t, elevator> const&,\n    point_rtree<elevator_idx_t> const&,\n    osr::hash_set<osr::node_idx_t> const&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/elevators/parse_elevator_id_osm_mapping.h",
    "content": "#pragma once\n\n#include <cinttypes>\n#include <filesystem>\n#include <string_view>\n\n#include \"motis/types.h\"\n\nnamespace motis {\n\nusing elevator_id_osm_mapping_t = hash_map<std::uint64_t, std::string>;\n\nelevator_id_osm_mapping_t parse_elevator_id_osm_mapping(std::string_view);\nelevator_id_osm_mapping_t parse_elevator_id_osm_mapping(\n    std::filesystem::path const&);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/elevators/parse_fasta.h",
    "content": "#pragma once\n\n#include <filesystem>\n#include <string_view>\n\n#include \"boost/json/object.hpp\"\n\n#include \"motis/types.h\"\n\nnamespace motis {\n\nstd::vector<nigiri::interval<nigiri::unixtime_t>> parse_out_of_service(\n    boost::json::object const&);\nvector_map<elevator_idx_t, elevator> parse_fasta(std::string_view);\nvector_map<elevator_idx_t, elevator> parse_fasta(std::filesystem::path const&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/elevators/parse_siri_fm.h",
    "content": "#pragma once\n\n#include <filesystem>\n#include <string_view>\n\n#include \"motis/types.h\"\n\nnamespace motis {\n\nvector_map<elevator_idx_t, elevator> parse_siri_fm(std::string_view);\nvector_map<elevator_idx_t, elevator> parse_siri_fm(\n    std::filesystem::path const&);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/elevators/update_elevators.h",
    "content": "#pragma once\n\n#include <memory>\n#include <string_view>\n\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis {\n\nstd::unique_ptr<elevators> update_elevators(config const&,\n                                            data const&,\n                                            std::string_view fasta_json,\n                                            nigiri::rt_timetable&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/endpoints/adr/filter_conv.h",
    "content": "#pragma once\n\n#include \"adr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis {\n\nadr::filter_type to_filter_type(\n    std::optional<motis::api::LocationTypeEnum> const&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/endpoints/adr/geocode.h",
    "content": "#pragma once\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis::ep {\n\nstruct geocode {\n  api::geocode_response operator()(boost::urls::url_view const& url) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  nigiri::timetable const* tt_;\n  tag_lookup const* tags_;\n  adr::typeahead const& t_;\n  adr::formatter const& f_;\n  adr::cache& cache_;\n  adr_ext const* ae_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/adr/reverse_geocode.h",
    "content": "#pragma once\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis::ep {\n\nstruct reverse_geocode {\n  api::reverseGeocode_response operator()(\n      boost::urls::url_view const& url) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  nigiri::timetable const* tt_;\n  tag_lookup const* tags_;\n  adr::typeahead const& t_;\n  adr::formatter const& f_;\n  adr::reverse const& r_;\n  adr_ext const* ae_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/adr/suggestions_to_response.h",
    "content": "#pragma once\n\n#include \"adr/adr.h\"\n#include \"adr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\napi::geocode_response suggestions_to_response(\n    adr::typeahead const&,\n    adr::formatter const&,\n    adr_ext const*,\n    nigiri::timetable const*,\n    tag_lookup const*,\n    osr::ways const* w,\n    osr::platforms const* pl,\n    platform_matches_t const* matches,\n    basic_string<adr::language_idx_t> const& lang_indices,\n    std::vector<adr::token> const& token_pos,\n    std::vector<adr::suggestion> const&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/endpoints/elevators.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct elevators {\n  boost::json::value operator()(boost::json::value const&) const;\n\n  std::shared_ptr<rt> const& rt_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/graph.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct graph {\n  boost::json::value operator()(boost::json::value const&) const;\n\n  osr::ways const& w_;\n  osr::lookup const& l_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/gtfsrt.h",
    "content": "#pragma once\n\n#include \"net/web_server/query_router.h\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct gtfsrt {\n  net::reply operator()(net::route_request const&, bool) const;\n\n  config const& config_;\n  nigiri::timetable const* tt_;\n  tag_lookup const* tags_;\n  std::shared_ptr<rt> const& rt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/initial.h",
    "content": "#pragma once\n\n#include \"boost/url/url.hpp\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\napi::initial_response get_initial_response(data const&);\n\nstruct initial {\n  api::initial_response operator()(boost::urls::url_view const&) const;\n\n  api::initial_response const& response_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/levels.h",
    "content": "#pragma once\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct levels {\n  api::levels_response operator()(boost::urls::url_view const&) const;\n\n  osr::ways const& w_;\n  osr::lookup const& l_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/map/flex_locations.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n#include \"boost/url/url_view.hpp\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis::ep {\n\nstruct flex_locations {\n  boost::json::value operator()(boost::urls::url_view const&) const;\n\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n  point_rtree<nigiri::location_idx_t> const& loc_rtree_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/map/rental.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct rental {\n  api::rentals_response operator()(boost::urls::url_view const&) const;\n\n  std::shared_ptr<gbfs::gbfs_data> const& gbfs_;\n  nigiri::timetable const* tt_;\n  tag_lookup const* tags_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/map/route_details.h",
    "content": "#pragma once\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis::ep {\n\nstruct route_details {\n  api::routeDetails_response operator()(boost::urls::url_view const&) const;\n\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n  std::shared_ptr<rt> const& rt_;\n  nigiri::shapes_storage const* shapes_;\n  railviz_static_index const& static_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/map/routes.h",
    "content": "#pragma once\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis::ep {\n\nstruct routes {\n  api::routes_response operator()(boost::urls::url_view const&) const;\n\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n  std::shared_ptr<rt> const& rt_;\n  nigiri::shapes_storage const* shapes_;\n  railviz_static_index const& static_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/map/shapes_debug.h",
    "content": "#pragma once\n\n#include \"net/web_server/query_router.h\"\n\n#include \"motis/config.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct shapes_debug {\n  net::reply operator()(net::route_request const&, bool) const;\n\n  config const& c_;\n  osr::ways const* w_;\n  osr::lookup const* l_;\n  nigiri::timetable const* tt_;\n  tag_lookup const* tags_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/map/stops.h",
    "content": "#pragma once\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis::ep {\n\nstruct stops {\n  api::stops_response operator()(boost::urls::url_view const&) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  point_rtree<nigiri::location_idx_t> const& loc_rtree_;\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/map/trips.h",
    "content": "#pragma once\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis::ep {\n\nstruct trips {\n  api::trips_response operator()(boost::urls::url_view const&) const;\n\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n  std::shared_ptr<rt> const& rt_;\n  nigiri::shapes_storage const* shapes_;\n  railviz_static_index const& static_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/matches.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis::ep {\n\nstruct matches {\n  boost::json::value operator()(boost::json::value const&) const;\n\n  point_rtree<nigiri::location_idx_t> const& loc_rtree_;\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  osr::platforms const& pl_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/metrics.h",
    "content": "#pragma once\n\n#include <string_view>\n\n#include \"net/web_server/query_router.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/metrics_registry.h\"\n\nnamespace motis::ep {\n\nstruct metrics {\n  net::reply operator()(net::route_request const&, bool) const;\n\n  nigiri::timetable const* tt_;\n  tag_lookup const* tags_;\n  std::shared_ptr<rt> const& rt_;\n  metrics_registry* metrics_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/ojp.h",
    "content": "#pragma once\n\n#include <optional>\n\n#include \"net/web_server/query_router.h\"\n\n#include \"motis/endpoints/adr/geocode.h\"\n#include \"motis/endpoints/map/stops.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/endpoints/stop_times.h\"\n#include \"motis/endpoints/trip.h\"\n\nnamespace motis::ep {\n\nstruct ojp {\n  net::reply operator()(net::route_request const&, bool) const;\n\n  std::optional<routing> routing_ep_;\n  std::optional<geocode> geocoding_ep_;\n  std::optional<stops> stops_ep_;\n  std::optional<stop_times> stop_times_ep_;\n  std::optional<trip> trip_ep_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/one_to_all.h",
    "content": "#pragma once\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/data.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct one_to_all {\n  api::Reachable operator()(boost::urls::url_view const&) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::lookup const* l_;\n  osr::platforms const* pl_;\n  osr::elevation_storage const* elevations_;\n  nigiri::timetable const& tt_;\n  std::shared_ptr<rt> const& rt_;\n  tag_lookup const& tags_;\n  flex::flex_areas const* fa_;\n  point_rtree<nigiri::location_idx_t> const* loc_tree_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  way_matches_storage const* way_matches_;\n  std::shared_ptr<gbfs::gbfs_data> const& gbfs_;\n  metrics_registry* metrics_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/one_to_many.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"utl/to_vec.h\"\n\n#include \"net/bad_request_exception.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"osr/location.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n\n#include \"motis/data.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/metrics_registry.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/place.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis::ep {\n\napi::oneToMany_response one_to_many_direct(\n    config const&,\n    osr::ways const&,\n    osr::lookup const&,\n    api::ModeEnum,\n    osr::location const& one,\n    std::vector<osr::location> const& many,\n    double max_direct_time,\n    double max_matching_distance,\n    osr::direction,\n    osr_parameters const&,\n    api::PedestrianProfileEnum,\n    api::ElevationCostsEnum,\n    osr::elevation_storage const*,\n    bool with_distance);\n\ntemplate <typename Params>\napi::oneToMany_response one_to_many_handle_request(\n    config const& config,\n    Params const& query,\n    osr::ways const& w,\n    osr::lookup const& l,\n    osr::elevation_storage const* elevations,\n    metrics_registry* metrics) {\n  metrics->one_to_many_requests_.Increment();\n  // required field with default value, not std::optional\n  static_assert(std::is_same_v<decltype(query.withDistance_), bool>);\n\n  auto const one = parse_location(query.one_, ';');\n  utl::verify<net::bad_request_exception>(\n      one.has_value(), \"{} is not a valid geo coordinate\", query.one_);\n\n  auto const many = utl::to_vec(query.many_, [](auto&& x) {\n    auto const y = parse_location(x, ';');\n    utl::verify<net::bad_request_exception>(\n        y.has_value(), \"{} is not a valid geo coordinate\", x);\n    return *y;\n  });\n\n  return one_to_many_direct(\n      config, w, l, query.mode_, *one, many, query.max_,\n      query.maxMatchingDistance_,\n      query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward,\n      get_osr_parameters(query), api::PedestrianProfileEnum::FOOT,\n      query.elevationCosts_, elevations, query.withDistance_);\n}\n\ntemplate <typename Endpoint, typename Query>\napi::OneToManyIntermodalResponse run_one_to_many_intermodal(\n    Endpoint const&,\n    Query const&,\n    place_t const& one,\n    std::vector<place_t> const& many);\n\nstruct one_to_many {\n  api::oneToMany_response operator()(boost::urls::url_view const&) const;\n\n  config const& config_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  osr::elevation_storage const* elevations_;\n  metrics_registry* metrics_;\n};\n\nstruct one_to_many_intermodal {\n  api::OneToManyIntermodalResponse operator()(\n      boost::urls::url_view const&) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::lookup const* l_;\n  osr::platforms const* pl_;\n  osr::elevation_storage const* elevations_;\n  nigiri::timetable const& tt_;\n  std::shared_ptr<rt> const& rt_;\n  tag_lookup const& tags_;\n  flex::flex_areas const* fa_;\n  point_rtree<nigiri::location_idx_t> const* loc_tree_;\n  platform_matches_t const* matches_;\n  way_matches_storage const* way_matches_;\n  std::shared_ptr<gbfs::gbfs_data> const& gbfs_;\n  metrics_registry* metrics_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/one_to_many_post.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n\n#include \"motis/data.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis::ep {\n\nstruct one_to_many_post {\n  api::oneToManyPost_response operator()(\n      motis::api::OneToManyParams const&) const;\n\n  config const& config_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  osr::elevation_storage const* elevations_;\n  metrics_registry* metrics_;\n};\n\nstruct one_to_many_intermodal_post {\n  api::OneToManyIntermodalResponse operator()(\n      api::OneToManyIntermodalParams const&) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::lookup const* l_;\n  osr::platforms const* pl_;\n  osr::elevation_storage const* elevations_;\n  nigiri::timetable const& tt_;\n  std::shared_ptr<rt> const& rt_;\n  tag_lookup const& tags_;\n  flex::flex_areas const* fa_;\n  point_rtree<nigiri::location_idx_t> const* loc_tree_;\n  platform_matches_t const* matches_;\n  way_matches_storage const* way_matches_;\n  std::shared_ptr<gbfs::gbfs_data> const& gbfs_;\n  metrics_registry* metrics_;\n};\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/osr_routing.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct osr_routing {\n  boost::json::value operator()(boost::json::value const&) const;\n\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  std::shared_ptr<rt> const& rt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/platforms.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct platforms {\n  boost::json::value operator()(boost::json::value const&) const;\n\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  osr::platforms const& pl_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/routing.h",
    "content": "#pragma once\n\n#include <chrono>\n#include <optional>\n#include <utility>\n#include <vector>\n\n#include \"boost/thread/tss.hpp\"\n\n#include \"osr/types.h\"\n\n#include \"nigiri/routing/clasz_mask.h\"\n#include \"nigiri/routing/raptor/raptor_state.h\"\n#include \"nigiri/routing/raptor_search.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/place.h\"\n\nnamespace motis::ep {\n\nconstexpr auto const kInfinityDuration =\n    nigiri::duration_t{std::numeric_limits<nigiri::duration_t::rep>::max()};\n\n// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)\nextern boost::thread_specific_ptr<osr::bitvec<osr::node_idx_t>> blocked;\n\nusing stats_map_t = std::map<std::string, std::uint64_t>;\n\nnigiri::interval<nigiri::unixtime_t> shrink(\n    bool keep_late,\n    std::size_t max_size,\n    nigiri::interval<nigiri::unixtime_t> search_interval,\n    std::vector<nigiri::routing::journey>& journeys);\n\nstd::vector<nigiri::routing::offset> station_start(nigiri::location_idx_t);\n\nstd::vector<nigiri::routing::via_stop> get_via_stops(\n    nigiri::timetable const&,\n    tag_lookup const&,\n    std::optional<std::vector<std::string>> const& vias,\n    std::vector<std::int64_t> const& times,\n    bool reverse);\n\nstd::vector<api::ModeEnum> deduplicate(std::vector<api::ModeEnum>);\n\nvoid remove_slower_than_fastest_direct(nigiri::routing::query&);\n\nstruct routing {\n  api::plan_response operator()(boost::urls::url_view const&) const;\n\n  std::vector<nigiri::routing::offset> get_offsets(\n      nigiri::rt_timetable const*,\n      place_t const&,\n      osr::direction,\n      std::vector<api::ModeEnum> const&,\n      std::optional<std::vector<api::RentalFormFactorEnum>> const&,\n      std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&,\n      std::optional<std::vector<std::string>> const& rental_providers,\n      std::optional<std::vector<std::string>> const& rental_provider_groups,\n      bool ignore_rental_return_constraints,\n      osr_parameters const&,\n      api::PedestrianProfileEnum,\n      api::ElevationCostsEnum,\n      std::chrono::seconds max,\n      double max_matching_distance,\n      gbfs::gbfs_routing_data&,\n      stats_map_t& stats) const;\n\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n  get_td_offsets(nigiri::rt_timetable const* rtt,\n                 elevators const*,\n                 place_t const&,\n                 osr::direction,\n                 std::vector<api::ModeEnum> const&,\n                 osr_parameters const&,\n                 api::PedestrianProfileEnum,\n                 api::ElevationCostsEnum,\n                 double max_matching_distance,\n                 std::chrono::seconds max,\n                 nigiri::routing::start_time_t const&,\n                 stats_map_t& stats) const;\n\n  std::pair<std::vector<api::Itinerary>, nigiri::duration_t> route_direct(\n      elevators const*,\n      gbfs::gbfs_routing_data&,\n      nigiri::lang_t const&,\n      api::Place const& from,\n      api::Place const& to,\n      std::vector<api::ModeEnum> const&,\n      std::optional<std::vector<api::RentalFormFactorEnum>> const&,\n      std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&,\n      std::optional<std::vector<std::string>> const& rental_providers,\n      std::optional<std::vector<std::string>> const& rental_provider_groups,\n      bool ignore_rental_return_constraints,\n      nigiri::unixtime_t time,\n      bool arrive_by,\n      osr_parameters const&,\n      api::PedestrianProfileEnum,\n      api::ElevationCostsEnum,\n      std::chrono::seconds max,\n      double max_matching_distance,\n      double fastest_direct_factor,\n      bool detailed_legs,\n      unsigned api_version) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::lookup const* l_;\n  osr::platforms const* pl_;\n  osr::elevation_storage const* elevations_;\n  nigiri::timetable const* tt_;\n  nigiri::routing::tb::tb_data const* tbd_;\n  tag_lookup const* tags_;\n  point_rtree<nigiri::location_idx_t> const* loc_tree_;\n  flex::flex_areas const* fa_;\n  platform_matches_t const* matches_;\n  way_matches_storage const* way_matches_;\n  std::shared_ptr<rt> const& rt_;\n  nigiri::shapes_storage const* shapes_;\n  std::shared_ptr<gbfs::gbfs_data> const& gbfs_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  odm::bounds const* odm_bounds_;\n  odm::ride_sharing_bounds const* ride_sharing_bounds_;\n  metrics_registry* metrics_;\n};\n\nbool is_intermodal(routing const&, place_t const&);\n\nnigiri::routing::location_match_mode get_match_mode(routing const&,\n                                                    place_t const&);\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "include/motis/endpoints/stop_times.h",
    "content": "#pragma once\n\n#include \"nigiri/types.h\"\n\n#include \"osr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/types.h\"\n\nnamespace motis::ep {\n\nstruct stop_times {\n  api::stoptimes_response operator()(boost::urls::url_view const&) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  point_rtree<nigiri::location_idx_t> const& loc_rtree_;\n  nigiri::timetable const& tt_;\n  tag_lookup const& tags_;\n  std::shared_ptr<rt> const& rt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/tiles.h",
    "content": "#pragma once\n\n#include \"net/web_server/query_router.h\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis::ep {\n\nstruct tiles {\n  net::reply operator()(net::route_request const&, bool) const;\n\n  tiles_data& tiles_data_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/transfers.h",
    "content": "#pragma once\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"nigiri/types.h\"\n\n#include \"osr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/types.h\"\n\nnamespace motis::ep {\n\nstruct transfers {\n  api::transfers_response operator()(boost::urls::url_view const&) const;\n\n  config const& c_;\n  tag_lookup const& tags_;\n  nigiri::timetable const& tt_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  osr::platforms const& pl_;\n  point_rtree<nigiri::location_idx_t> const& loc_rtree_;\n  platform_matches_t const& matches_;\n  std::shared_ptr<rt> rt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/trip.h",
    "content": "#pragma once\n\n#include \"boost/url/url_view.hpp\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis::ep {\n\nstruct trip {\n  api::Itinerary operator()(boost::urls::url_view const&) const;\n\n  config const& config_;\n  osr::ways const* w_;\n  osr::lookup const* l_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  nigiri::timetable const& tt_;\n  nigiri::shapes_storage const* shapes_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  tag_lookup const& tags_;\n  point_rtree<nigiri::location_idx_t> const& loc_tree_;\n  std::shared_ptr<rt> const& rt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/endpoints/update_elevator.h",
    "content": "#pragma once\n\n#include \"boost/json/value.hpp\"\n\n#include \"utl/init_from.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"osr/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/types.h\"\n\nnamespace motis::ep {\n\nstruct update_elevator {\n  boost::json::value operator()(boost::json::value const&) const;\n\n  config const& c_;\n  nigiri::timetable const& tt_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  osr::platforms const& pl_;\n  point_rtree<nigiri::location_idx_t> const& loc_rtree_;\n  hash_set<osr::node_idx_t> const& elevator_nodes_;\n  elevator_id_osm_mapping_t const* elevator_ids_;\n  platform_matches_t const& matches_;\n  std::shared_ptr<rt>& rt_;\n};\n\n}  // namespace motis::ep"
  },
  {
    "path": "include/motis/flex/flex.h",
    "content": "#pragma once\n\n#include \"osr/location.h\"\n#include \"osr/routing/profile.h\"\n#include \"osr/types.h\"\n\n#include \"nigiri/routing/query.h\"\n\n#include \"motis/flex/mode_id.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n\nnamespace motis::flex {\n\nusing flex_routings_t =\n    hash_map<std::pair<nigiri::flex_stop_seq_idx_t, nigiri::stop_idx_t>,\n             std::vector<mode_id>>;\n\nosr::sharing_data prepare_sharing_data(nigiri::timetable const&,\n                                       osr::ways const&,\n                                       osr::lookup const&,\n                                       osr::platforms const*,\n                                       flex_areas const&,\n                                       platform_matches_t const*,\n                                       mode_id,\n                                       osr::direction,\n                                       flex_routing_data&);\n\nbool is_in_flex_stop(nigiri::timetable const&,\n                     osr::ways const&,\n                     flex_areas const&,\n                     flex_routing_data const&,\n                     nigiri::flex_stop_t const&,\n                     osr::node_idx_t);\n\nflex_routings_t get_flex_routings(nigiri::timetable const&,\n                                  point_rtree<nigiri::location_idx_t> const&,\n                                  nigiri::routing::start_time_t,\n                                  geo::latlng const&,\n                                  osr::direction,\n                                  std::chrono::seconds max);\n\nvoid add_flex_td_offsets(osr::ways const&,\n                         osr::lookup const&,\n                         osr::platforms const*,\n                         platform_matches_t const*,\n                         way_matches_storage const*,\n                         nigiri::timetable const&,\n                         flex_areas const&,\n                         point_rtree<nigiri::location_idx_t> const&,\n                         nigiri::routing::start_time_t,\n                         osr::location const&,\n                         osr::direction,\n                         std::chrono::seconds max,\n                         double const max_matching_distance,\n                         osr_parameters const&,\n                         flex_routing_data&,\n                         nigiri::routing::td_offsets_t&,\n                         std::map<std::string, std::uint64_t>& stats);\n\n}  // namespace motis::flex\n"
  },
  {
    "path": "include/motis/flex/flex_areas.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"tg.h\"\n\n#include \"osr/types.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/gbfs/compression.h\"\n#include \"motis/types.h\"\n\nnamespace motis::flex {\n\nstruct flex_areas {\n  explicit flex_areas(nigiri::timetable const&,\n                      osr::ways const&,\n                      osr::lookup const&);\n  ~flex_areas();\n\n  void add_area(nigiri::flex_area_idx_t,\n                osr::bitvec<osr::node_idx_t>&,\n                osr::bitvec<osr::node_idx_t>& tmp) const;\n\n  bool is_in_area(nigiri::flex_area_idx_t, geo::latlng const&) const;\n\n  vector_map<nigiri::flex_area_idx_t, gbfs::compressed_bitvec> area_nodes_;\n  vector_map<nigiri::flex_area_idx_t, tg_geom*> idx_;\n};\n\n}  // namespace motis::flex"
  },
  {
    "path": "include/motis/flex/flex_output.h",
    "content": "#pragma once\n\n#include \"motis/flex/flex_routing_data.h\"\n#include \"motis/flex/mode_id.h\"\n#include \"motis/osr/street_routing.h\"\n\nnamespace motis::flex {\n\nstd::string_view get_flex_stop_name(nigiri::timetable const&,\n                                    nigiri::lang_t const&,\n                                    nigiri::flex_stop_t const&);\n\nstd::string_view get_flex_id(nigiri::timetable const&,\n                             nigiri::flex_stop_t const&);\n\nstruct flex_output : public output {\n  flex_output(osr::ways const&,\n              osr::lookup const&,\n              osr::platforms const*,\n              platform_matches_t const*,\n              adr_ext const*,\n              tz_map_t const*,\n              tag_lookup const&,\n              nigiri::timetable const&,\n              flex_areas const&,\n              mode_id);\n  ~flex_output() override;\n\n  api::ModeEnum get_mode() const override;\n  osr::search_profile get_profile() const override;\n  bool is_time_dependent() const override;\n  transport_mode_t get_cache_key() const override;\n  osr::sharing_data const* get_sharing_data() const override;\n  void annotate_leg(nigiri::lang_t const&,\n                    osr::node_idx_t,\n                    osr::node_idx_t,\n                    api::Leg&) const override;\n  api::Place get_place(\n      nigiri::lang_t const&,\n      osr::node_idx_t,\n      std::optional<std::string> const& fallback_tz) const override;\n\n  std::size_t get_additional_node_idx(osr::node_idx_t) const;\n\nprivate:\n  osr::ways const& w_;\n  osr::platforms const* pl_;\n  platform_matches_t const* matches_;\n  adr_ext const* ae_;\n  tz_map_t const* tz_;\n  nigiri::timetable const& tt_;\n  tag_lookup const& tags_;\n  flex_areas const& fa_;\n  flex::flex_routing_data flex_routing_data_;\n  osr::sharing_data sharing_data_;\n  mode_id mode_id_;\n};\n\n}  // namespace motis::flex"
  },
  {
    "path": "include/motis/flex/flex_routing_data.h",
    "content": "#pragma once\n\n#include <vector>\n\n#include \"osr/routing/additional_edge.h\"\n#include \"osr/routing/sharing_data.h\"\n#include \"osr/types.h\"\n#include \"osr/ways.h\"\n\n#include \"nigiri/types.h\"\n\nnamespace motis::flex {\n\nstruct flex_routing_data {\n  osr::sharing_data to_sharing_data() {\n    return {.start_allowed_ = &start_allowed_,\n            .end_allowed_ = &end_allowed_,\n            .through_allowed_ = &through_allowed_,\n            .additional_node_offset_ = additional_node_offset_,\n            .additional_node_coordinates_ = additional_node_coordinates_,\n            .additional_edges_ = additional_edges_};\n  }\n\n  nigiri::location_idx_t get_additional_node(osr::node_idx_t const n) const {\n    return additional_nodes_[to_idx(n - additional_node_offset_)];\n  }\n\n  osr::bitvec<osr::node_idx_t> start_allowed_;\n  osr::bitvec<osr::node_idx_t> through_allowed_;\n  osr::bitvec<osr::node_idx_t> end_allowed_;\n  osr::node_idx_t::value_t additional_node_offset_;\n  std::vector<geo::latlng> additional_node_coordinates_;\n  osr::hash_map<osr::node_idx_t, std::vector<osr::additional_edge>>\n      additional_edges_;\n  std::vector<nigiri::location_idx_t> additional_nodes_;\n};\n\n}  // namespace motis::flex\n"
  },
  {
    "path": "include/motis/flex/mode_id.h",
    "content": "#pragma once\n\n#include \"osr/types.h\"\n\n#include \"nigiri/types.h\"\n\nnamespace motis::flex {\n\nstruct mode_id {\n  mode_id(nigiri::flex_transport_idx_t const t,\n          nigiri::stop_idx_t const stop_idx,\n          osr::direction const dir)\n      : transport_{t},\n        dir_{dir != osr::direction::kForward},\n        stop_idx_{stop_idx},\n        msb_{1U} {}\n\n  static bool is_flex(nigiri::transport_mode_id_t const x) {\n    return (x & 0x80'00'00'00) == 0x80'00'00'00;\n  }\n\n  explicit mode_id(nigiri::transport_mode_id_t const x) {\n    std::memcpy(this, &x, sizeof(mode_id));\n  }\n\n  osr::direction get_dir() const {\n    return dir_ == 0 ? osr::direction::kForward : osr::direction::kBackward;\n  }\n\n  nigiri::stop_idx_t get_stop() const {\n    return static_cast<nigiri::stop_idx_t>(stop_idx_);\n  }\n\n  nigiri::flex_transport_idx_t get_flex_transport() const {\n    return nigiri::flex_transport_idx_t{transport_};\n  }\n\n  nigiri::transport_mode_id_t to_id() const {\n    static_assert(sizeof(mode_id) == sizeof(nigiri::transport_mode_id_t));\n    auto id = nigiri::transport_mode_id_t{};\n    std::memcpy(&id, this, sizeof(id));\n    return id;\n  }\n\n  nigiri::flex_transport_idx_t::value_t transport_ : 23;\n  nigiri::flex_transport_idx_t::value_t dir_ : 1;\n  nigiri::flex_transport_idx_t::value_t stop_idx_ : 7;\n  nigiri::flex_transport_idx_t::value_t msb_ : 1;\n};\n\n}  // namespace motis::flex"
  },
  {
    "path": "include/motis/fwd.h",
    "content": "#pragma once\n\nnamespace adr {\nstruct formatter;\nstruct reverse;\nstruct area_database;\nstruct typeahead;\nstruct cache;\n}  // namespace adr\n\nnamespace osr {\n\nstruct location;\nstruct sharing_data;\nstruct ways;\nstruct platforms;\nstruct lookup;\nstruct elevation_storage;\n\n}  // namespace osr\n\nnamespace nigiri {\n\nstruct timetable;\nstruct rt_timetable;\nstruct shapes_storage;\n\nnamespace rt {\nstruct run;\nstruct run_stop;\n}  // namespace rt\n\nnamespace routing {\nstruct td_offset;\nstruct offset;\n\nnamespace tb {\nstruct tb_data;\n}\n\n}  // namespace routing\n\n}  // namespace nigiri\n\nnamespace motis {\n\nstruct tiles_data;\nstruct rt;\nstruct tag_lookup;\nstruct config;\nstruct railviz_static_index;\nstruct railviz_rt_index;\nstruct elevators;\nstruct metrics_registry;\nstruct way_matches_storage;\nstruct data;\nstruct adr_ext;\n\nnamespace odm {\nstruct bounds;\nstruct ride_sharing_bounds;\n}  // namespace odm\n\nnamespace gbfs {\nstruct gbfs_data;\nstruct gbfs_routing_data;\n}  // namespace gbfs\n\nnamespace flex {\nstruct flex_routing_data;\nstruct flex_areas;\n}  // namespace flex\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/gbfs/compression.h",
    "content": "#pragma once\n\n#include <cstdint>\n#include <cstdlib>\n#include <memory>\n\n#include \"cista/containers/bitvec.h\"\n\n#include \"utl/verify.h\"\n\n#include \"lz4.h\"\n\n#include \"motis/gbfs/data.h\"\n\nnamespace motis::gbfs {\n\ntemplate <typename Vec, typename Key = typename Vec::size_type>\ninline compressed_bitvec compress_bitvec(\n    cista::basic_bitvec<Vec, Key> const& bv) {\n  auto const* original_data = reinterpret_cast<char const*>(bv.blocks_.data());\n  auto const original_bytes =\n      static_cast<int>(bv.blocks_.size() *\n                       sizeof(typename cista::basic_bitvec<Vec, Key>::block_t));\n  auto const max_compressed_size = LZ4_compressBound(original_bytes);\n\n  auto cbv = compressed_bitvec{\n      .data_ =\n          std::unique_ptr<char[], compressed_bitvec::free_deleter>{\n              static_cast<char*>(\n                  std::malloc(static_cast<std::size_t>(max_compressed_size)))},\n      .original_bytes_ = original_bytes,\n      .bitvec_size_ = bv.size_};\n  utl::verify(cbv.data_ != nullptr,\n              \"could not allocate memory for compressed bitvec\");\n\n  cbv.compressed_bytes_ = LZ4_compress_default(\n      original_data, cbv.data_.get(), original_bytes, max_compressed_size);\n  utl::verify(cbv.compressed_bytes_ > 0, \"could not compress bitvec\");\n\n  if (auto* compressed = std::realloc(\n          cbv.data_.get(), static_cast<std::size_t>(cbv.compressed_bytes_));\n      compressed != nullptr) {\n    cbv.data_.release();\n    cbv.data_.reset(static_cast<char*>(compressed));\n  }\n  return cbv;\n}\n\ntemplate <typename Vec, typename Key = typename Vec::size_type>\ninline void decompress_bitvec(compressed_bitvec const& cbv,\n                              cista::basic_bitvec<Vec, Key>& bv) {\n  bv.resize(static_cast<typename cista::basic_bitvec<Vec, Key>::size_type>(\n      cbv.bitvec_size_));\n  auto const decompressed_bytes = LZ4_decompress_safe(\n      cbv.data_.get(), reinterpret_cast<char*>(bv.blocks_.data()),\n      cbv.compressed_bytes_,\n      static_cast<int>(\n          bv.blocks_.size() *\n          sizeof(typename cista::basic_bitvec<Vec, Key>::block_t)));\n  utl::verify(decompressed_bytes == cbv.original_bytes_,\n              \"could not decompress bitvec\");\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/data.h",
    "content": "#pragma once\n\n#include <algorithm>\n#include <chrono>\n#include <compare>\n#include <cstdint>\n#include <filesystem>\n#include <map>\n#include <memory>\n#include <mutex>\n#include <optional>\n#include <string>\n#include <utility>\n#include <variant>\n#include <vector>\n\n#include \"tg.h\"\n\n#include \"cista/hash.h\"\n#include \"cista/strong.h\"\n\n#include \"geo/box.h\"\n#include \"geo/latlng.h\"\n\n#include \"utl/helpers/algorithm.h\"\n\n#include \"osr/routing/additional_edge.h\"\n#include \"osr/routing/sharing_data.h\"\n#include \"osr/types.h\"\n\n#include \"motis/box_rtree.h\"\n#include \"motis/config.h\"\n#include \"motis/fwd.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/types.h\"\n\n#include \"motis/gbfs/lru_cache.h\"\n\nnamespace motis::gbfs {\n\nenum class gbfs_version : std::uint8_t {\n  k1 = 0,\n  k2 = 1,\n  k3 = 2,\n};\n\nusing vehicle_type_idx_t =\n    cista::strong<std::uint16_t, struct vehicle_type_idx_>;\n\nenum class vehicle_form_factor : std::uint8_t {\n  kBicycle = 0,\n  kCargoBicycle = 1,\n  kCar = 2,\n  kMoped = 3,\n  kScooterStanding = 4,\n  kScooterSeated = 5,\n  kOther = 6\n};\n\nenum class propulsion_type : std::uint8_t {\n  kHuman = 0,\n  kElectricAssist = 1,\n  kElectric = 2,\n  kCombustion = 3,\n  kCombustionDiesel = 4,\n  kHybrid = 5,\n  kPlugInHybrid = 6,\n  kHydrogenFuelCell = 7\n};\n\nenum class return_constraint : std::uint8_t {\n  kFreeFloating = 0,  // includes hybrid\n  kAnyStation = 1,\n  kRoundtripStation = 2\n};\n\nstruct vehicle_type {\n  std::string id_{};\n  vehicle_type_idx_t idx_{vehicle_type_idx_t::invalid()};\n  std::string name_{};\n  vehicle_form_factor form_factor_{};\n  propulsion_type propulsion_type_{};\n  return_constraint return_constraint_{};\n  bool known_return_constraint_{};  // true if taken from feed, false if guessed\n};\n\nstruct temp_vehicle_type {\n  std::string id_;\n  std::string name_;\n  vehicle_form_factor form_factor_{};\n  propulsion_type propulsion_type_{};\n};\n\nenum class vehicle_start_type : std::uint8_t {\n  kStation = 0,\n  kFreeFloating = 1\n};\n\nstruct system_information {\n  std::string id_;\n  std::string name_;\n  std::string name_short_;\n  std::string operator_;\n  std::string url_;\n  std::string purchase_url_;\n  std::string mail_;\n  std::string color_;\n};\n\nstruct rental_uris {\n  // all fields are optional\n  std::string android_;\n  std::string ios_;\n  std::string web_;\n};\n\nstruct tg_geom_deleter {\n  void operator()(tg_geom* ptr) const {\n    if (ptr != nullptr) {\n      tg_geom_free(ptr);\n    }\n  }\n};\n\nstruct station_information {\n  std::string id_;\n  std::string name_;\n  geo::latlng pos_{};\n  // optional:\n  std::string address_{};\n  std::string cross_street_{};\n  rental_uris rental_uris_{};\n\n  std::shared_ptr<tg_geom> station_area_{};\n\n  geo::box bounding_box() const {\n    if (station_area_) {\n      auto const rect = tg_geom_rect(station_area_.get());\n      return geo::box{geo::latlng{rect.min.y, rect.min.x},\n                      geo::latlng{rect.max.y, rect.max.x}};\n    } else {\n      return geo::box{pos_, pos_};\n    }\n  }\n};\n\nstruct station_status {\n  unsigned num_vehicles_available_{};\n  hash_map<vehicle_type_idx_t, unsigned> vehicle_types_available_{};\n  hash_map<vehicle_type_idx_t, unsigned> vehicle_docks_available_{};\n  bool is_renting_{true};\n  bool is_returning_{true};\n};\n\nstruct station {\n  station_information info_{};\n  station_status status_{};\n};\n\nstruct vehicle_status {\n  bool operator==(vehicle_status const& o) const { return id_ == o.id_; }\n  auto operator<=>(vehicle_status const& o) const { return id_ <=> o.id_; }\n\n  std::string id_;\n  geo::latlng pos_;\n  bool is_reserved_{};\n  bool is_disabled_{};\n  vehicle_type_idx_t vehicle_type_idx_;\n  std::string station_id_;\n  std::string home_station_id_;\n  rental_uris rental_uris_{};\n};\n\nstruct rule {\n  bool allows_rental_operation() const {\n    return ride_start_allowed_ || ride_end_allowed_ || ride_through_allowed_ ||\n           station_parking_.value_or(false);\n  }\n\n  std::vector<vehicle_type_idx_t> vehicle_type_idxs_{};\n  bool ride_start_allowed_{};\n  bool ride_end_allowed_{};\n  bool ride_through_allowed_{};\n  std::optional<bool> station_parking_{};\n};\n\nstruct geofencing_restrictions {\n  bool ride_start_allowed_{true};\n  bool ride_end_allowed_{true};\n  bool ride_through_allowed_{true};\n  std::optional<bool> station_parking_{};\n};\n\nstruct zone {\n  zone() = default;\n  zone(tg_geom* geom, std::vector<rule>&& rules, std::string&& name)\n      : geom_{geom, tg_geom_deleter{}},\n        rules_{std::move(rules)},\n        clockwise_{geom_ && tg_geom_num_polys(geom_.get()) > 0\n                       ? tg_poly_clockwise(tg_geom_poly_at(geom_.get(), 0))\n                       : true},\n        name_{std::move(name)} {}\n\n  geo::box bounding_box() const {\n    auto const rect = tg_geom_rect(geom_.get());\n    return geo::box{geo::latlng{rect.min.y, rect.min.x},\n                    geo::latlng{rect.max.y, rect.max.x}};\n  }\n\n  bool allows_rental_operation() const {\n    return utl::any_of(\n        rules_, [](rule const& r) { return r.allows_rental_operation(); });\n  }\n\n  std::shared_ptr<tg_geom> geom_;\n  std::vector<rule> rules_;\n  bool clockwise_{true};\n  std::string name_;\n};\n\nstruct geofencing_zones {\n  gbfs_version version_{};\n  std::vector<zone> zones_;\n  std::vector<rule> global_rules_;\n\n  void clear();\n  geofencing_restrictions get_restrictions(\n      geo::latlng const& pos,\n      vehicle_type_idx_t,\n      geofencing_restrictions const& default_restrictions) const;\n};\n\nstruct additional_node {\n  struct station {\n    std::string id_;\n  };\n\n  struct vehicle {\n    std::size_t idx_{};\n  };\n\n  std::variant<station, vehicle> data_;\n};\n\nstruct file_info {\n  bool has_value() const { return expiry_.has_value(); }\n\n  bool needs_update(std::chrono::system_clock::time_point const now) const {\n    return !expiry_.has_value() || *expiry_ < now;\n  }\n\n  std::optional<std::chrono::system_clock::time_point> expiry_{};\n  cista::hash_t hash_{};\n};\n\nstruct provider_file_infos {\n  bool needs_update() const {\n    auto const now = std::chrono::system_clock::now();\n    return urls_fi_.needs_update(now) ||\n           system_information_fi_.needs_update(now) ||\n           vehicle_types_fi_.needs_update(now) ||\n           station_information_fi_.needs_update(now) ||\n           station_status_fi_.needs_update(now) ||\n           vehicle_status_fi_.needs_update(now) ||\n           geofencing_zones_fi_.needs_update(now);\n  }\n\n  hash_map<std::string, std::string> urls_{};\n\n  file_info urls_fi_{};\n  file_info system_information_fi_{};\n  file_info vehicle_types_fi_{};\n  file_info station_information_fi_{};\n  file_info station_status_fi_{};\n  file_info vehicle_status_fi_{};\n  file_info geofencing_zones_fi_{};\n};\n\nstruct compressed_bitvec {\n  struct free_deleter {\n    void operator()(char* p) const { std::free(p); }\n  };\n\n  std::unique_ptr<char[], free_deleter> data_{};\n  int original_bytes_{};\n  int compressed_bytes_{};\n  std::size_t bitvec_size_{};\n};\n\nstruct routing_data {\n  std::vector<additional_node> additional_nodes_{};\n  std::vector<geo::latlng> additional_node_coordinates_;\n  osr::hash_map<osr::node_idx_t, std::vector<osr::additional_edge>>\n      additional_edges_{};\n\n  osr::bitvec<osr::node_idx_t> start_allowed_{};\n  osr::bitvec<osr::node_idx_t> end_allowed_{};\n  osr::bitvec<osr::node_idx_t> through_allowed_{};\n  bool station_parking_{};\n};\n\nstruct compressed_routing_data {\n  std::vector<additional_node> additional_nodes_{};\n  std::vector<geo::latlng> additional_node_coordinates_;\n  osr::hash_map<osr::node_idx_t, std::vector<osr::additional_edge>>\n      additional_edges_{};\n  compressed_bitvec start_allowed_{};\n  compressed_bitvec end_allowed_{};\n  compressed_bitvec through_allowed_{};\n};\n\nstruct provider_routing_data;\n\nstruct products_routing_data {\n  products_routing_data(std::shared_ptr<provider_routing_data const>&& prd,\n                        compressed_routing_data const& compressed);\n\n  osr::sharing_data get_sharing_data(\n      osr::node_idx_t::value_t const additional_node_offset,\n      bool ignore_return_constraints) const {\n    return {.start_allowed_ = &start_allowed_,\n            .end_allowed_ = ignore_return_constraints ? nullptr : &end_allowed_,\n            .through_allowed_ = &through_allowed_,\n            .additional_node_offset_ = additional_node_offset,\n            .additional_node_coordinates_ =\n                compressed_.additional_node_coordinates_,\n            .additional_edges_ = compressed_.additional_edges_};\n  }\n\n  std::shared_ptr<provider_routing_data const> provider_routing_data_;\n  compressed_routing_data const& compressed_;\n\n  osr::bitvec<osr::node_idx_t> start_allowed_;\n  osr::bitvec<osr::node_idx_t> end_allowed_;\n  osr::bitvec<osr::node_idx_t> through_allowed_;\n};\n\nusing gbfs_products_idx_t =\n    cista::strong<std::uint16_t, struct gbfs_products_idx_>;\n\nstruct provider_routing_data\n    : std::enable_shared_from_this<provider_routing_data> {\n  std::shared_ptr<products_routing_data> get_products_routing_data(\n      gbfs_products_idx_t const prod_idx) const {\n    return std::make_shared<products_routing_data>(\n        shared_from_this(), products_.at(to_idx(prod_idx)));\n  }\n\n  std::vector<compressed_routing_data> products_;\n};\n\nstruct provider_products {\n  bool includes_vehicle_type(vehicle_type_idx_t const idx) const {\n    return (idx == vehicle_type_idx_t::invalid() && vehicle_types_.empty()) ||\n           utl::find(vehicle_types_, idx) != end(vehicle_types_);\n  }\n\n  gbfs_products_idx_t idx_{gbfs_products_idx_t::invalid()};\n  std::vector<vehicle_type_idx_t> vehicle_types_;\n  vehicle_form_factor form_factor_{vehicle_form_factor::kBicycle};\n  propulsion_type propulsion_type_{propulsion_type::kHuman};\n  return_constraint return_constraint_{};\n  bool known_return_constraint_{};  // true if taken from feed, false if guessed\n\n  bool has_vehicles_to_rent_{};\n};\n\nstruct gbfs_products_ref {\n  friend bool operator==(gbfs_products_ref const&,\n                         gbfs_products_ref const&) = default;\n\n  explicit operator bool() const noexcept {\n    return provider_ != gbfs_provider_idx_t::invalid();\n  }\n\n  gbfs_provider_idx_t provider_{gbfs_provider_idx_t::invalid()};\n  gbfs_products_idx_t products_{gbfs_products_idx_t::invalid()};\n};\n\nstruct gbfs_provider {\n  std::string id_;  // from config\n  gbfs_provider_idx_t idx_{gbfs_provider_idx_t::invalid()};\n  std::string group_id_;\n\n  std::shared_ptr<provider_file_infos> file_infos_{};\n\n  system_information sys_info_{};\n  std::map<std::string, station> stations_{};\n  vector_map<vehicle_type_idx_t, vehicle_type> vehicle_types_{};\n  hash_map<std::pair<std::string, vehicle_start_type>, vehicle_type_idx_t>\n      vehicle_types_map_{};\n  hash_map<std::string, temp_vehicle_type> temp_vehicle_types_{};\n  std::vector<vehicle_status> vehicle_status_;\n  geofencing_zones geofencing_zones_{};\n  geofencing_restrictions default_restrictions_{};\n  std::optional<return_constraint> default_return_constraint_{};\n\n  vector_map<gbfs_products_idx_t, provider_products> products_;\n  bool has_vehicles_to_rent_{};\n  geo::box bbox_{};\n\n  std::optional<std::string> color_{};\n};\n\nstruct gbfs_group {\n  std::string id_;\n  std::string name_;\n  std::optional<std::string> color_{};\n\n  std::vector<gbfs_provider_idx_t> providers_{};\n};\n\nstruct oauth_state {\n  config::gbfs::oauth_settings settings_;\n  std::string access_token_{};\n  std::optional<std::chrono::system_clock::time_point> expiry_{};\n  unsigned expires_in_{};\n};\n\nstruct provider_feed {\n  bool operator==(provider_feed const& o) const { return id_ == o.id_; }\n  bool operator==(std::string const& id) const { return id_ == id; }\n  bool operator<(provider_feed const& o) const { return id_ < o.id_; }\n\n  std::string id_;\n  std::string url_;\n  headers_t headers_{};\n  std::optional<std::filesystem::path> dir_{};\n  geofencing_restrictions default_restrictions_{};\n  std::optional<return_constraint> default_return_constraint_{};\n  std::optional<std::string> config_group_{};\n  std::optional<std::string> config_color_{};\n  std::shared_ptr<oauth_state> oauth_{};\n  std::map<std::string, unsigned> default_ttl_{};\n  std::map<std::string, unsigned> overwrite_ttl_{};\n};\n\nstruct aggregated_feed {\n  bool operator==(aggregated_feed const& o) const { return id_ == o.id_; }\n  bool operator==(std::string const& id) const { return id_ == id; }\n  bool operator<(aggregated_feed const& o) const { return id_ < o.id_; }\n\n  bool needs_update() const {\n    return !expiry_.has_value() ||\n           expiry_.value() < std::chrono::system_clock::now();\n  }\n\n  std::string id_;\n  std::string url_;\n  headers_t headers_{};\n  std::optional<std::chrono::system_clock::time_point> expiry_{};\n  std::vector<provider_feed> feeds_{};\n  std::shared_ptr<oauth_state> oauth_{};\n  std::map<std::string, unsigned> default_ttl_{};\n  std::map<std::string, unsigned> overwrite_ttl_{};\n};\n\nstruct gbfs_data {\n  explicit gbfs_data(std::size_t const cache_size) : cache_{cache_size} {}\n\n  std::shared_ptr<products_routing_data> get_products_routing_data(\n      osr::ways const& w, osr::lookup const& l, gbfs_products_ref);\n\n  std::shared_ptr<std::vector<std::unique_ptr<provider_feed>>>\n      standalone_feeds_{};\n  std::shared_ptr<std::vector<std::unique_ptr<aggregated_feed>>>\n      aggregated_feeds_{};\n\n  vector_map<gbfs_provider_idx_t, std::unique_ptr<gbfs_provider>> providers_{};\n  hash_map<std::string, gbfs_provider_idx_t> provider_by_id_{};\n  point_rtree<gbfs_provider_idx_t> provider_rtree_{};\n  box_rtree<gbfs_provider_idx_t> provider_zone_rtree_{};\n\n  hash_map<std::string, gbfs_group> groups_{};\n\n  lru_cache<gbfs_provider_idx_t, provider_routing_data> cache_;\n\n  // used to share decompressed routing data between routing requests\n  std::mutex products_routing_data_mutex_;\n  hash_map<gbfs_products_ref, std::weak_ptr<products_routing_data>>\n      products_routing_data_{};\n};\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/gbfs_output.h",
    "content": "#pragma once\n\n#include \"motis/osr/street_routing.h\"\n\nnamespace motis::gbfs {\n\nstruct gbfs_output final : public output {\n  gbfs_output(osr::ways const&,\n              gbfs::gbfs_routing_data&,\n              gbfs::gbfs_products_ref,\n              bool ignore_rental_return_constraints);\n  ~gbfs_output() override;\n\n  api::ModeEnum get_mode() const override;\n\n  osr::search_profile get_profile() const override;\n\n  bool is_time_dependent() const override;\n  transport_mode_t get_cache_key() const override;\n\n  osr::sharing_data const* get_sharing_data() const override;\n\n  void annotate_leg(nigiri::lang_t const&,\n                    osr::node_idx_t const from_node,\n                    osr::node_idx_t const to_node,\n                    api::Leg&) const override;\n\n  api::Place get_place(nigiri::lang_t const&,\n                       osr::node_idx_t,\n                       std::optional<std::string> const& tz) const override;\n\n  std::size_t get_additional_node_idx(osr::node_idx_t const n) const;\n\nprivate:\n  std::string get_node_name(osr::node_idx_t) const;\n\n  osr::ways const& w_;\n  gbfs::gbfs_routing_data& gbfs_rd_;\n  gbfs::gbfs_provider const& provider_;\n  gbfs::provider_products const& products_;\n  gbfs::products_routing_data const* prod_rd_;\n  osr::sharing_data sharing_data_;\n  api::Rental rental_;\n};\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/geofencing.h",
    "content": "#pragma once\n\n#include <string>\n#include <vector>\n\n#include \"tg.h\"\n\n#include \"geo/latlng.h\"\n\n#include \"motis/gbfs/data.h\"\n\nnamespace motis::gbfs {\n\nbool applies(std::vector<vehicle_type_idx_t> const& rule_vehicle_type_idxs,\n             std::vector<vehicle_type_idx_t> const& segment_vehicle_type_idxs);\nbool multipoly_contains_point(tg_geom const* geom, geo::latlng const& pos);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/lru_cache.h",
    "content": "#pragma once\n\n#include <future>\n#include <memory>\n#include <mutex>\n#include <shared_mutex>\n#include <vector>\n\n#include \"motis/types.h\"\n\n#include \"utl/helpers/algorithm.h\"\n\nnamespace motis::gbfs {\n\ntemplate <typename Key, typename Value>\nclass lru_cache {\npublic:\n  explicit lru_cache(std::size_t const max_size) : max_size_{max_size} {}\n\n  lru_cache(lru_cache const& o) : max_size_{o.max_size_} {\n    auto read_lock = std::shared_lock{o.mutex_};\n    cache_map_ = o.cache_map_;\n    lru_order_ = o.lru_order_;\n    pending_computations_.clear();\n  }\n\n  lru_cache& operator=(lru_cache const& o) {\n    if (this != &o) {\n      auto read_lock = std::shared_lock{o.mutex_};\n      auto write_lock = std::unique_lock{mutex_};\n      max_size_ = o.max_size_;\n      cache_map_ = o.cache_map_;\n      lru_order_ = o.lru_order_;\n      pending_computations_.clear();\n    }\n    return *this;\n  }\n\n  template <typename F>\n  std::shared_ptr<Value> get_or_compute(Key const key, F compute_fn) {\n    // check with shared lock if entry already exists\n    {\n      auto read_lock = std::shared_lock{mutex_};\n      if (auto it = cache_map_.find(key); it != cache_map_.end()) {\n        move_to_front(key);\n        return it->second->value_;\n      }\n    }\n\n    // not found -> acquire exclusive lock to modify the cache\n    auto write_lock = std::unique_lock{mutex_};\n\n    // check again in case another thread inserted it\n    if (auto it = cache_map_.find(key); it != cache_map_.end()) {\n      move_to_front(key);\n      return it->second->value_;\n    }\n\n    // if another thread is already computing it, wait for it\n    if (auto it = pending_computations_.find(key);\n        it != pending_computations_.end()) {\n      auto future = it->second;\n      write_lock.unlock();\n      return future.get();\n    }\n\n    // create pending computation\n    auto promise = std::promise<std::shared_ptr<Value>>{};\n    auto shared_future = promise.get_future().share();\n    pending_computations_[key] = shared_future;\n    write_lock.unlock();\n\n    // compute the value\n    auto value = compute_fn();\n\n    // store the result\n    write_lock.lock();\n\n    if (lru_order_.size() >= max_size_) {\n      // evict least recently used cache entry\n      auto const last_key = lru_order_.back();\n      cache_map_.erase(last_key);\n      lru_order_.pop_back();\n    }\n\n    cache_map_.try_emplace(\n        key, std::make_shared<cache_entry>(cache_entry{key, value}));\n    lru_order_.insert(lru_order_.begin(), key);\n    pending_computations_.erase(key);\n    promise.set_value(value);\n\n    return value;\n  }\n\n  std::shared_ptr<Value> get(Key const key) {\n    auto read_lock = std::shared_lock{mutex_};\n    if (auto it = cache_map_.find(key); it != cache_map_.end()) {\n      return it->second->value_;\n    }\n    return nullptr;\n  }\n\n  bool contains(Key const key) {\n    auto read_lock = std::shared_lock{mutex_};\n    return cache_map_.find(key) != cache_map_.end();\n  }\n\n  template <typename F>\n  void update_if_exists(Key const key, F update_fn) {\n    auto write_lock = std::unique_lock{mutex_};\n    if (auto it = cache_map_.find(key); it != cache_map_.end()) {\n      it->second->value_ = update_fn(it->second->value_);\n      move_to_front(key);\n    }\n  }\n\n  /// adds an entry to the cache if there is still space or updates\n  /// an existing entry if it already exists\n  template <typename F>\n  bool try_add_or_update(Key const key, F compute_fn) {\n    auto write_lock = std::unique_lock{mutex_};\n\n    if (auto it = cache_map_.find(key); it != cache_map_.end()) {\n      it->second->value_ = compute_fn();\n      move_to_front(key);\n      return true;\n    }\n\n    if (lru_order_.size() >= max_size_) {\n      return false;\n    }\n\n    cache_map_.try_emplace(\n        key, std::make_shared<cache_entry>(cache_entry{key, compute_fn()}));\n    lru_order_.insert(lru_order_.begin(), key);\n    return true;\n  }\n\n  void remove(Key const key) {\n    auto write_lock = std::unique_lock{mutex_};\n    if (auto it = cache_map_.find(key); it != cache_map_.end()) {\n      if (auto const lru_it = utl::find(lru_order_, key);\n          lru_it != lru_order_.end()) {\n        lru_order_.erase(lru_it);\n      }\n      cache_map_.erase(it);\n    }\n  }\n\n  std::vector<std::pair<Key, std::shared_ptr<Value>>> get_all_entries() const {\n    auto read_lock = std::shared_lock{mutex_};\n    auto entries = std::vector<std::pair<Key, std::shared_ptr<Value>>>{};\n    entries.reserve(lru_order_.size());\n    for (auto const it = lru_order_.rbegin(); it != lru_order_.rend(); ++it) {\n      if (auto const map_it = cache_map_.find(*it);\n          map_it != cache_map_.end()) {\n        entries.emplace_back(map_it->first, map_it->second->value_);\n      }\n    }\n    return entries;\n  }\n\n  std::size_t size() const { return lru_order_.size(); }\n\n  bool empty() const { return lru_order_.empty(); }\n\nprivate:\n  struct cache_entry {\n    Key key_{};\n    std::shared_ptr<Value> value_{};\n  };\n\n  void move_to_front(Key const key) {\n    auto const it = utl::find(lru_order_, key);\n    if (it != lru_order_.end()) {\n      lru_order_.erase(it);\n      lru_order_.insert(lru_order_.begin(), key);\n    }\n  }\n\n  std::size_t max_size_;\n  hash_map<Key, std::shared_ptr<cache_entry>> cache_map_;\n  std::vector<Key> lru_order_;\n  hash_map<Key, std::shared_future<std::shared_ptr<Value>>>\n      pending_computations_{};\n  mutable std::shared_mutex mutex_;\n};\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/mode.h",
    "content": "#pragma once\n\n#include <optional>\n#include <vector>\n\n#include \"motis/gbfs/data.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis::gbfs {\n\napi::RentalFormFactorEnum to_api_form_factor(vehicle_form_factor);\nvehicle_form_factor from_api_form_factor(api::RentalFormFactorEnum);\n\napi::RentalPropulsionTypeEnum to_api_propulsion_type(propulsion_type);\npropulsion_type from_api_propulsion_type(api::RentalPropulsionTypeEnum);\n\napi::RentalReturnConstraintEnum to_api_return_constraint(return_constraint);\n\nbool products_match(\n    provider_products const& prod,\n    std::optional<std::vector<api::RentalFormFactorEnum>> const& form_factors,\n    std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n        propulsion_types);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/osr_mapping.h",
    "content": "#pragma once\n\n#include \"motis/fwd.h\"\n\nnamespace motis::gbfs {\n\nstruct gbfs_provider;\nstruct provider_routing_data;\n\nvoid map_data(osr::ways const&,\n              osr::lookup const&,\n              gbfs_provider const&,\n              provider_routing_data&);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/osr_profile.h",
    "content": "#pragma once\n\n#include \"osr/routing/profile.h\"\n\n#include \"motis/gbfs/data.h\"\n\nnamespace motis::gbfs {\n\nosr::search_profile get_osr_profile(vehicle_form_factor const&);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/parser.h",
    "content": "#pragma once\n\n#include <string>\n\n#include \"boost/json.hpp\"\n\n#include \"motis/gbfs/data.h\"\n#include \"motis/types.h\"\n\nnamespace motis::gbfs {\n\nhash_map<std::string, std::string> parse_discovery(\n    boost::json::value const& root);\n\nstd::optional<return_constraint> parse_return_constraint(std::string_view s);\n\nvoid load_system_information(gbfs_provider&, boost::json::value const& root);\nvoid load_station_information(gbfs_provider&, boost::json::value const& root);\nvoid load_station_status(gbfs_provider&, boost::json::value const& root);\nvoid load_vehicle_types(gbfs_provider&, boost::json::value const& root);\nvoid load_vehicle_status(gbfs_provider&, boost::json::value const& root);\nvoid load_geofencing_zones(gbfs_provider&, boost::json::value const& root);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/partition.h",
    "content": "#pragma once\n\n#include <cassert>\n#include <cstdint>\n#include <span>\n#include <vector>\n\n#include \"cista/strong.h\"\n\nnamespace motis::gbfs {\n\ntemplate <typename T>\nstruct partition {\n  explicit partition(T const n) : n_{n} {\n    partition_.resize(static_cast<std::size_t>(cista::to_idx(n)));\n    for (auto i = T{0}; i < n; ++i) {\n      partition_[static_cast<std::size_t>(cista::to_idx(i))] = i;\n    }\n    if (n != 0) {\n      // initially there's only one set ending at n-1\n      set_ends_.push_back(n - 1);\n    }\n  }\n\n  void refine(std::span<T const> const s) {\n    if (s.empty()) {\n      return;\n    }\n\n    // mark elements in s\n    auto in_s =\n        std::vector<bool>(static_cast<std::size_t>(cista::to_idx(n_)), false);\n    for (auto const elem : s) {\n      assert(elem < n_);\n      in_s[static_cast<std::size_t>(cista::to_idx(elem))] = true;\n    }\n\n    // process each existing set\n    auto current_start = T{0};\n    auto new_set_ends = std::vector<T>{};\n    new_set_ends.reserve(2 * set_ends_.size());\n\n    for (auto const set_end : set_ends_) {\n      // count elements in current set that are in s\n      auto count = T{0};\n      for (auto i = current_start; i <= set_end; ++i) {\n        if (in_s[static_cast<std::size_t>(cista::to_idx(\n                partition_[static_cast<std::size_t>(cista::to_idx(i))]))]) {\n          ++count;\n        }\n      }\n\n      auto const set_size = set_end - current_start + 1;\n      // if split is needed (some but not all elements are in s)\n      if (count != 0 && count != set_size) {\n        // partition the set into two parts\n        auto split_pos = current_start;\n        for (auto i = current_start; i <= set_end; ++i) {\n          if (in_s[static_cast<std::size_t>(cista::to_idx(\n                  partition_[static_cast<std::size_t>(cista::to_idx(i))]))]) {\n            // move element to front of split\n            if (i != split_pos) {\n              std::swap(partition_[static_cast<std::size_t>(cista::to_idx(i))],\n                        partition_[static_cast<std::size_t>(\n                            cista::to_idx(split_pos))]);\n            }\n            ++split_pos;\n          }\n        }\n\n        // add end positions for both new sets\n        new_set_ends.push_back(split_pos - 1);\n        new_set_ends.push_back(set_end);\n      } else {\n        // no split needed, keep original set\n        new_set_ends.push_back(set_end);\n      }\n\n      current_start = set_end + 1;\n    }\n\n    set_ends_ = std::move(new_set_ends);\n  }\n\n  std::vector<std::vector<T>> get_sets() const {\n    auto result = std::vector<std::vector<T>>{};\n    result.reserve(set_ends_.size());\n\n    auto current_start = T{0};\n    for (auto const set_end : set_ends_) {\n      auto set = std::vector<T>{};\n      set.reserve(\n          static_cast<std::size_t>(cista::to_idx(set_end - current_start + 1)));\n      for (auto i = current_start; i <= set_end; ++i) {\n        set.push_back(partition_[static_cast<std::size_t>(cista::to_idx(i))]);\n      }\n      result.push_back(std::move(set));\n      current_start = set_end + 1;\n    }\n\n    return result;\n  }\n\n  // the number of elements in the partition - the original set\n  // contains the elements 0, 1, ..., n - 1\n  T n_;\n\n  // stores the elements grouped by sets - the elements of each set are\n  // stored contiguously, e.g. \"0345789\" could be {{0, 3, 4}, {5}, {7, 8, 9}}\n  // or {{0}, {3, 4}, {5}, {7, 8, 9}}, depending on set_ends_.\n  std::vector<T> partition_;\n\n  // stores the end index of each set in partition_\n  std::vector<T> set_ends_;\n};\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/routing_data.h",
    "content": "#pragma once\n\n#include <cstdint>\n#include <memory>\n\n#include \"motis/fwd.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/types.h\"\n\nnamespace motis::gbfs {\n\nstruct gbfs_routing_data {\n  gbfs_routing_data() = default;\n  gbfs_routing_data(osr::ways const* w,\n                    osr::lookup const* l,\n                    std::shared_ptr<gbfs_data> data)\n      : w_{w}, l_{l}, data_{std::move(data)} {}\n\n  bool has_data() const { return data_ != nullptr; }\n\n  std::shared_ptr<provider_routing_data> get_provider_routing_data(\n      gbfs_provider const&);\n\n  products_routing_data* get_products_routing_data(\n      gbfs_provider const& provider, gbfs_products_idx_t prod_idx);\n  products_routing_data* get_products_routing_data(gbfs_products_ref);\n\n  provider_products const& get_products(gbfs_products_ref);\n\n  nigiri::transport_mode_id_t get_transport_mode(gbfs_products_ref);\n  gbfs_products_ref get_products_ref(nigiri::transport_mode_id_t) const;\n\n  osr::ways const* w_{};\n  osr::lookup const* l_{};\n  std::shared_ptr<gbfs_data> data_{};\n\n  hash_map<gbfs_products_ref, std::shared_ptr<products_routing_data>> products_;\n  std::vector<gbfs_products_ref> products_refs_;\n  hash_map<gbfs_products_ref, nigiri::transport_mode_id_t>\n      products_ref_to_transport_mode_;\n};\n\nstd::shared_ptr<provider_routing_data> compute_provider_routing_data(\n    osr::ways const&, osr::lookup const&, gbfs_provider const&);\n\nstd::shared_ptr<provider_routing_data> get_provider_routing_data(\n    osr::ways const&, osr::lookup const&, gbfs_data&, gbfs_provider const&);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/gbfs/update.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"boost/asio/awaitable.hpp\"\n#include \"boost/asio/io_context.hpp\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis::gbfs {\n\nboost::asio::awaitable<void> update(config const&,\n                                    osr::ways const&,\n                                    osr::lookup const&,\n                                    std::shared_ptr<gbfs_data>&);\n\nvoid run_gbfs_update(boost::asio::io_context&,\n                     config const&,\n                     osr::ways const&,\n                     osr::lookup const&,\n                     std::shared_ptr<gbfs_data>&);\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "include/motis/get_loc.h",
    "content": "#pragma once\n\n#include \"osr/platforms.h\"\n#include \"osr/routing/route.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\ninline osr::location get_loc(\n    nigiri::timetable const& tt,\n    osr::ways const& w,\n    osr::platforms const& pl,\n    vector_map<nigiri::location_idx_t, osr::platform_idx_t> const& matches,\n    nigiri::location_idx_t const l) {\n  auto pos = tt.locations_.coordinates_[l];\n  if (matches[l] != osr::platform_idx_t::invalid()) {\n    auto const center = get_platform_center(pl, w, matches[l]);\n    if (center.has_value() && geo::distance(*center, pos) < kMaxAdjust) {\n      pos = *center;\n    }\n  }\n  auto const lvl = matches[l] == osr::platform_idx_t::invalid()\n                       ? osr::level_t{0.F}\n                       : pl.get_level(w, matches[l]);\n  return {pos, lvl};\n}\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/get_stops_with_traffic.h",
    "content": "#pragma once\n\n#include \"nigiri/types.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace motis {\n\nstd::vector<nigiri::location_idx_t> get_stops_with_traffic(\n    nigiri::timetable const&,\n    nigiri::rt_timetable const*,\n    point_rtree<nigiri::location_idx_t> const&,\n    osr::location const&,\n    double const distance,\n    nigiri::location_idx_t const not_equal_to =\n        nigiri::location_idx_t::invalid());\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/hashes.h",
    "content": "#pragma once\n\n#include <filesystem>\n#include <map>\n#include <string>\n#include <utility>\n\nnamespace motis {\n\nusing meta_entry_t = std::pair<std::string, std::uint64_t>;\nusing meta_t = std::map<std::string, std::uint64_t>;\n\nconstexpr auto const osr_version = []() {\n  return meta_entry_t{\"osr_bin_ver\", 34U};\n};\nconstexpr auto const adr_version = []() {\n  return meta_entry_t{\"adr_bin_ver\", 14U};\n};\nconstexpr auto const adr_ext_version = []() {\n  return meta_entry_t{\"adr_ext_bin_ver\", 4U};\n};\nconstexpr auto const n_version = []() {\n  return meta_entry_t{\"nigiri_bin_ver\", 33U};\n};\nconstexpr auto const tbd_version = []() {\n  return meta_entry_t{\"tbd_bin_ver\", 1U};\n};\nconstexpr auto const matches_version = []() {\n  return meta_entry_t{\"matches_bin_ver\", 5U};\n};\nconstexpr auto const tiles_version = []() {\n  return meta_entry_t{\"tiles_bin_ver\", 1U};\n};\nconstexpr auto const osr_footpath_version = []() {\n  return meta_entry_t{\"osr_footpath_bin_ver\", 3U};\n};\nconstexpr auto const routed_shapes_version = []() {\n  return meta_entry_t{\"routed_shapes_ver\", 10U};\n};\n\nstd::string to_str(meta_t const&);\n\nmeta_t read_hashes(std::filesystem::path const& data_path,\n                   std::string const& name);\n\nvoid write_hashes(std::filesystem::path const& data_path,\n                  std::string const& name,\n                  meta_t const& h);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/http_req.h",
    "content": "#pragma once\n\n#include <chrono>\n#include <map>\n#include <string>\n\n#include \"boost/asio/awaitable.hpp\"\n#include \"boost/beast/http/dynamic_body.hpp\"\n#include \"boost/beast/http/message.hpp\"\n#include \"boost/url/url.hpp\"\n\nnamespace motis {\n\nconstexpr auto const kBodySizeLimit = 512U * 1024U * 1024U;  // 512 M\n\nusing http_response =\n    boost::beast::http::response<boost::beast::http::dynamic_body>;\n\nstruct proxy {\n  bool use_tls_;\n  std::string host_, port_;\n};\n\nboost::asio::awaitable<http_response> http_GET(\n    boost::urls::url,\n    std::map<std::string, std::string> const& headers,\n    std::chrono::seconds timeout,\n    std::optional<proxy> const& = std::nullopt);\n\nboost::asio::awaitable<http_response> http_POST(\n    boost::urls::url,\n    std::map<std::string, std::string> const& headers,\n    std::string const& body,\n    std::chrono::seconds timeout,\n    std::optional<proxy> const& = std::nullopt);\n\nstd::string get_http_body(http_response const&);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/import.h",
    "content": "#pragma once\n\n#include <filesystem>\n\n#include \"motis/config.h\"\n\nnamespace motis {\n\nvoid import(config const&,\n            std::filesystem::path const& data_path,\n            std::optional<std::vector<std::string>> const& task_filter = {});\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/journey_to_response.h",
    "content": "#pragma once\n\n#include <string_view>\n#include <variant>\n\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/types.h\"\n\n#include \"osr/location.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/osr/street_routing.h\"\n#include \"motis/place.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\ndouble get_level(osr::ways const*,\n                 osr::platforms const*,\n                 platform_matches_t const*,\n                 nigiri::location_idx_t);\n\nstd::optional<std::vector<api::Alert>> get_alerts(\n    nigiri::rt::frun const&,\n    std::optional<std::pair<nigiri::rt::run_stop, nigiri::event_type>> const&,\n    bool fuzzy_stop,\n    std::optional<std::vector<std::string>> const& language);\n\napi::Itinerary journey_to_response(\n    osr::ways const*,\n    osr::lookup const*,\n    osr::platforms const*,\n    nigiri::timetable const&,\n    tag_lookup const&,\n    flex::flex_areas const*,\n    elevators const* e,\n    nigiri::rt_timetable const*,\n    platform_matches_t const* matches,\n    osr::elevation_storage const*,\n    nigiri::shapes_storage const*,\n    gbfs::gbfs_routing_data&,\n    adr_ext const*,\n    tz_map_t const*,\n    nigiri::routing::journey const&,\n    place_t const& start,\n    place_t const& dest,\n    street_routing_cache_t&,\n    osr::bitvec<osr::node_idx_t>* blocked_mem,\n    bool car_transfers,\n    osr_parameters const&,\n    api::PedestrianProfileEnum,\n    api::ElevationCostsEnum,\n    bool join_interlined_legs,\n    bool detailed_transfers,\n    bool detailed_legs,\n    bool with_fares,\n    bool with_scheduled_skipped_stops,\n    double timetable_max_matching_distance,\n    double max_matching_distance,\n    unsigned api_version,\n    bool ignore_start_rental_return_constraints,\n    bool ignore_dest_rental_return_constraints,\n    std::optional<std::vector<std::string>> const& language);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/location_routes.h",
    "content": "#pragma once\n\n#include \"nigiri/timetable.h\"\n\nnamespace motis {\n\ninline nigiri::hash_set<std::string_view> get_location_routes(\n    nigiri::timetable const& tt, nigiri::location_idx_t const l) {\n  auto names = nigiri::hash_set<std::string_view>{};\n  for (auto const r : tt.location_routes_[l]) {\n    for (auto const t : tt.route_transport_ranges_[r]) {\n      names.emplace(tt.transport_name(t));\n    }\n  }\n  return names;\n}\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/logging.h",
    "content": "#pragma once\n\n#include <string>\n\n#include \"motis/config.h\"\n\nnamespace motis {\n\nint set_log_level(config const&);\n\nint set_log_level(std::string&& log_lvl);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/match_platforms.h",
    "content": "#pragma once\n\n#include <map>\n#include <optional>\n\n#include \"osr/lookup.h\"\n#include \"osr/types.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/data.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\nusing platform_matches_t =\n    vector_map<nigiri::location_idx_t, osr::platform_idx_t>;\n\nstruct way_matches_storage {\n  way_matches_storage(std::filesystem::path,\n                      cista::mmap::protection,\n                      double max_matching_distance);\n\n  cista::mmap mm(char const* file);\n\n  cista::mmap::protection mode_;\n  std::filesystem::path p_;\n\n  osr::mm_vecvec<nigiri::location_idx_t, osr::raw_way_candidate> matches_;\n  double max_matching_distance_;\n\n  void preprocess_osr_matches(nigiri::timetable const&,\n                              osr::platforms const&,\n                              osr::ways const&,\n                              osr::lookup const&,\n                              platform_matches_t const&);\n};\n\nstd::optional<geo::latlng> get_platform_center(osr::platforms const&,\n                                               osr::ways const&,\n                                               osr::platform_idx_t);\n\nosr::platform_idx_t get_match(nigiri::timetable const&,\n                              osr::platforms const&,\n                              osr::ways const&,\n                              nigiri::location_idx_t);\n\nplatform_matches_t get_matches(nigiri::timetable const&,\n                               osr::platforms const&,\n                               osr::ways const&);\n\nstd::optional<std::string_view> get_track(std::string_view);\n\nstd::vector<osr::match_t> get_reverse_platform_way_matches(\n    osr::lookup const&,\n    way_matches_storage const*,\n    osr::search_profile,\n    std::span<nigiri::location_idx_t const>,\n    std::span<osr::location const>,\n    osr::direction,\n    double max_matching_distance);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/metrics_registry.h",
    "content": "#pragma once\n\n#include \"prometheus/counter.h\"\n#include \"prometheus/family.h\"\n#include \"prometheus/gauge.h\"\n#include \"prometheus/histogram.h\"\n#include \"prometheus/registry.h\"\n\nnamespace motis {\n\nstruct metrics_registry {\n  metrics_registry();\n  ~metrics_registry();\n  prometheus::Registry registry_;\n  prometheus::Counter& routing_requests_;\n  prometheus::Counter& one_to_many_requests_;\n  prometheus::Counter& routing_journeys_found_;\n  prometheus::Family<prometheus::Histogram>& routing_odm_journeys_found_;\n  prometheus::Histogram& routing_odm_journeys_found_blacklist_;\n  prometheus::Histogram& routing_odm_journeys_found_whitelist_;\n  prometheus::Histogram& routing_odm_journeys_found_non_dominated_pareto_;\n  prometheus::Histogram& routing_odm_journeys_found_non_dominated_cost_;\n  prometheus::Histogram& routing_odm_journeys_found_non_dominated_prod_;\n  prometheus::Histogram& routing_odm_journeys_found_non_dominated_;\n  prometheus::Histogram& routing_journey_duration_seconds_;\n  prometheus::Family<prometheus::Histogram>&\n      routing_execution_duration_seconds_;\n  prometheus::Histogram& routing_execution_duration_seconds_init_;\n  prometheus::Histogram& routing_execution_duration_seconds_blacklisting_;\n  prometheus::Histogram& routing_execution_duration_seconds_preparing_;\n  prometheus::Histogram& routing_execution_duration_seconds_routing_;\n  prometheus::Histogram& routing_execution_duration_seconds_whitelisting_;\n  prometheus::Histogram& routing_execution_duration_seconds_mixing_;\n  prometheus::Histogram& routing_execution_duration_seconds_total_;\n  prometheus::Family<prometheus::Gauge>& current_trips_running_scheduled_count_;\n  prometheus::Family<prometheus::Gauge>&\n      current_trips_running_scheduled_with_realtime_count_;\n  prometheus::Gauge& total_trips_with_realtime_count_;\n  prometheus::Family<prometheus::Gauge>& timetable_first_day_timestamp_;\n  prometheus::Family<prometheus::Gauge>& timetable_last_day_timestamp_;\n  prometheus::Family<prometheus::Gauge>& timetable_locations_count_;\n  prometheus::Family<prometheus::Gauge>& timetable_trips_count_;\n  prometheus::Family<prometheus::Gauge>& timetable_transports_x_days_count_;\n\nprivate:\n  metrics_registry(prometheus::Histogram::BucketBoundaries event_boundaries,\n                   prometheus::Histogram::BucketBoundaries time_boundaries);\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/motis_instance.h",
    "content": "#include <memory>\n#include <thread>\n\n#include \"boost/asio/io_context.hpp\"\n\n#include \"net/web_server/query_router.h\"\n\n#include \"utl/set_thread_name.h\"\n\n#include \"motis/endpoints/adr/geocode.h\"\n#include \"motis/endpoints/adr/reverse_geocode.h\"\n#include \"motis/endpoints/elevators.h\"\n#include \"motis/endpoints/graph.h\"\n#include \"motis/endpoints/gtfsrt.h\"\n#include \"motis/endpoints/initial.h\"\n#include \"motis/endpoints/levels.h\"\n#include \"motis/endpoints/map/flex_locations.h\"\n#include \"motis/endpoints/map/rental.h\"\n#include \"motis/endpoints/map/route_details.h\"\n#include \"motis/endpoints/map/routes.h\"\n#include \"motis/endpoints/map/shapes_debug.h\"\n#include \"motis/endpoints/map/stops.h\"\n#include \"motis/endpoints/map/trips.h\"\n#include \"motis/endpoints/matches.h\"\n#include \"motis/endpoints/metrics.h\"\n#include \"motis/endpoints/ojp.h\"\n#include \"motis/endpoints/one_to_all.h\"\n#include \"motis/endpoints/one_to_many.h\"\n#include \"motis/endpoints/one_to_many_post.h\"\n#include \"motis/endpoints/osr_routing.h\"\n#include \"motis/endpoints/platforms.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/endpoints/stop_times.h\"\n#include \"motis/endpoints/tiles.h\"\n#include \"motis/endpoints/transfers.h\"\n#include \"motis/endpoints/trip.h\"\n#include \"motis/endpoints/update_elevator.h\"\n#include \"motis/gbfs/update.h\"\n#include \"motis/metrics_registry.h\"\n#include \"motis/rt_update.h\"\n\nnamespace motis {\n\nstruct io_thread {\n  template <typename Fn>\n  io_thread(char const* name, Fn&& f) {\n    ioc_ = std::make_unique<boost::asio::io_context>();\n    t_ = std::make_unique<std::thread>(\n        [ioc = ioc_.get(), name, f = std::move(f)]() {\n          utl::set_current_thread_name(name);\n          f(*ioc);\n          ioc->run();\n        });\n  }\n\n  io_thread() = default;\n\n  void stop() {\n    if (ioc_ == nullptr) {\n      return;\n    }\n    ioc_->stop();\n  }\n\n  void join() {\n    if (t_ == nullptr) {\n      return;\n    }\n    t_->join();\n  }\n\n  std::unique_ptr<std::thread> t_;\n  std::unique_ptr<boost::asio::io_context> ioc_;\n};\n\ntemplate <typename Executor>\nstruct motis_instance {\n  motis_instance(Executor&& exec,\n                 data& d,\n                 config const& c,\n                 std::string_view motis_version)\n      : qr_{std::forward<Executor>(exec)} {\n    qr_.add_header(\"Server\", fmt::format(\"MOTIS {}\", motis_version));\n    d.motis_version_ = motis_version;\n    if (c.server_.value_or(config::server{}).data_attribution_link_) {\n      qr_.add_header(\"Link\", fmt::format(\"<{}>; rel=\\\"license\\\"\",\n                                         *c.server_->data_attribution_link_));\n    }\n\n    POST<ep::matches>(\"/api/matches\", d);\n    POST<ep::elevators>(\"/api/elevators\", d);\n    POST<ep::osr_routing>(\"/api/route\", d);\n    POST<ep::platforms>(\"/api/platforms\", d);\n    POST<ep::graph>(\"/api/graph\", d);\n    GET<ep::transfers>(\"/api/debug/transfers\", d);\n    GET<ep::flex_locations>(\"/api/debug/flex\", d);\n    GET<ep::levels>(\"/api/v1/map/levels\", d);\n    GET<ep::initial>(\"/api/v1/map/initial\", d);\n    GET<ep::reverse_geocode>(\"/api/v1/reverse-geocode\", d);\n    GET<ep::geocode>(\"/api/v1/geocode\", d);\n    GET<ep::routing>(\"/api/v1/plan\", d);\n    GET<ep::routing>(\"/api/v2/plan\", d);\n    GET<ep::routing>(\"/api/v3/plan\", d);\n    GET<ep::routing>(\"/api/v4/plan\", d);\n    GET<ep::routing>(\"/api/v5/plan\", d);\n    GET<ep::stop_times>(\"/api/v1/stoptimes\", d);\n    GET<ep::stop_times>(\"/api/v4/stoptimes\", d);\n    GET<ep::stop_times>(\"/api/v5/stoptimes\", d);\n    GET<ep::trip>(\"/api/v1/trip\", d);\n    GET<ep::trip>(\"/api/v2/trip\", d);\n    GET<ep::trip>(\"/api/v4/trip\", d);\n    GET<ep::trip>(\"/api/v5/trip\", d);\n    GET<ep::trips>(\"/api/v1/map/trips\", d);\n    GET<ep::trips>(\"/api/v4/map/trips\", d);\n    GET<ep::trips>(\"/api/v5/map/trips\", d);\n    GET<ep::stops>(\"/api/v1/map/stops\", d);\n    GET<ep::route_details>(\"/api/experimental/map/route-details\", d);\n    GET<ep::routes>(\"/api/experimental/map/routes\", d);\n    GET<ep::rental>(\"/api/v1/map/rentals\", d);\n    GET<ep::rental>(\"/api/v1/rentals\", d);\n    GET<ep::one_to_all>(\"/api/experimental/one-to-all\", d);\n    GET<ep::one_to_all>(\"/api/v1/one-to-all\", d);\n    GET<ep::one_to_many>(\"/api/v1/one-to-many\", d);\n    GET<ep::one_to_many_intermodal>(\"/api/experimental/one-to-many-intermodal\",\n                                    d);\n    POST<ep::one_to_many_intermodal_post>(\n        \"/api/experimental/one-to-many-intermodal\", d);\n    POST<ep::one_to_many_post>(\"/api/v1/one-to-many\", d);\n\n    if (!c.requires_rt_timetable_updates()) {\n      // Elevator updates are not compatible with RT-updates.\n      POST<ep::update_elevator>(\"/api/update_elevator\", d);\n    }\n\n    if (c.shapes_debug_api_enabled()) {\n      utl::verify(d.w_ != nullptr && d.l_ != nullptr && d.tt_ != nullptr &&\n                      d.tags_ != nullptr,\n                  \"data for shapes debug api not loaded\");\n      qr_.route(\"GET\", \"/api/experimental/shapes-debug/\",\n                ep::shapes_debug{c, d.w_.get(), d.l_.get(), d.tt_.get(),\n                                 d.tags_.get()});\n    }\n\n    if (c.tiles_) {\n      utl::verify(d.tiles_ != nullptr, \"tiles data not loaded\");\n      qr_.route(\"GET\", \"/tiles/\", ep::tiles{*d.tiles_});\n    }\n\n    qr_.route(\"POST\", \"/ojp20\",\n              ep::ojp{\n                  .routing_ep_ = utl::init_from<ep::routing>(d),\n                  .geocoding_ep_ = utl::init_from<ep::geocode>(d),\n                  .stops_ep_ = utl::init_from<ep::stops>(d),\n                  .stop_times_ep_ = utl::init_from<ep::stop_times>(d),\n                  .trip_ep_ = utl::init_from<ep::trip>(d),\n              });\n\n    qr_.route(\"GET\", \"/metrics\",\n              ep::metrics{d.tt_.get(), d.tags_.get(), d.rt_, d.metrics_.get()});\n    qr_.route(\"GET\", \"/gtfsrt\",\n              ep::gtfsrt{c, d.tt_.get(), d.tags_.get(), d.rt_});\n    qr_.serve_files(c.server_.value_or(config::server{}).web_folder_);\n    qr_.enable_cors();\n  }\n\n  template <typename T, typename From>\n  void GET(std::string target, From& from) {\n    if (auto x = utl::init_from<T>(from); x.has_value()) {\n      qr_.get(std::move(target), std::move(*x));\n    }\n  }\n\n  template <typename T, typename From>\n  void POST(std::string target, From& from) {\n    if (auto x = utl::init_from<T>(from); x.has_value()) {\n      qr_.post(std::move(target), std::move(*x));\n    }\n  }\n\n  void run(data& d, config const& c) {\n    if (d.w_ && d.l_ && c.has_gbfs_feeds()) {\n      gbfs_ = io_thread{\"motis gbfs update\", [&](boost::asio::io_context& ioc) {\n                          gbfs::run_gbfs_update(ioc, c, *d.w_, *d.l_, d.gbfs_);\n                        }};\n    }\n\n    if (c.requires_rt_timetable_updates()) {\n      rt_ = io_thread{\"motis rt update\", [&](boost::asio::io_context& ioc) {\n                        run_rt_update(ioc, c, d);\n                      }};\n    }\n  }\n\n  void stop() {\n    rt_.stop();\n    gbfs_.stop();\n  }\n\n  void join() {\n    rt_.join();\n    gbfs_.join();\n  }\n\n  net::query_router<Executor> qr_{};\n  io_thread rt_, gbfs_;\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/odm/bounds.h",
    "content": "#pragma once\n\n#include <filesystem>\n\n#include \"geo/latlng.h\"\n\nstruct tg_geom;\n\nnamespace motis::odm {\n\nstruct bounds {\n  explicit bounds(std::filesystem::path const&);\n  ~bounds();\n\n  bool contains(geo::latlng const&) const;\n\n  tg_geom* geom_{nullptr};\n};\n\nstruct ride_sharing_bounds : bounds {\n  using bounds::bounds;\n};\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/odm/journeys.h",
    "content": "#pragma once\n\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/routing/pareto_set.h\"\n\nnamespace motis::odm {\n\nstd::vector<nigiri::routing::journey> from_csv(std::string_view);\n\nnigiri::pareto_set<nigiri::routing::journey> separate_pt(\n    std::vector<nigiri::routing::journey>&);\n\nstd::string to_csv(nigiri::routing::journey const&);\n\nstd::string to_csv(std::vector<nigiri::routing::journey> const&);\n\nnigiri::routing::journey make_odm_direct(nigiri::location_idx_t from,\n                                         nigiri::location_idx_t to,\n                                         nigiri::unixtime_t departure,\n                                         nigiri::unixtime_t arrival);\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/odm/meta_router.h",
    "content": "#pragma once\n\n#include <optional>\n#include <variant>\n#include <vector>\n\n#include \"osr/location.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n\n#include \"motis/endpoints/routing.h\"\n#include \"motis/fwd.h\"\n#include \"motis/gbfs/routing_data.h\"\n#include \"motis/odm/query_factory.h\"\n#include \"motis/place.h\"\n\nnamespace nigiri {\nstruct timetable;\nstruct rt_timetable;\n}  // namespace nigiri\n\nnamespace nigiri::routing {\nstruct query;\nstruct journey;\n}  // namespace nigiri::routing\n\nnamespace motis::odm {\n\nstruct meta_router {\n  meta_router(ep::routing const&,\n              api::plan_params const&,\n              std::vector<api::ModeEnum> const& pre_transit_modes,\n              std::vector<api::ModeEnum> const& post_transit_modes,\n              std::vector<api::ModeEnum> const& direct_modes,\n              std::variant<osr::location, tt_location> const& from,\n              std::variant<osr::location, tt_location> const& to,\n              api::Place const& from_p,\n              api::Place const& to_p,\n              nigiri::routing::query const& start_time,\n              std::vector<api::Itinerary>& direct,\n              nigiri::duration_t fastest_direct_,\n              bool odm_pre_transit,\n              bool odm_post_transit,\n              bool odm_direct,\n              bool ride_sharing_pre_transit,\n              bool ride_sharing_post_transit,\n              bool ride_sharing_direct,\n              unsigned api_version);\n  ~meta_router();\n\n  api::plan_response run();\n\n  struct routing_result {\n    routing_result() = default;\n    routing_result(nigiri::routing::routing_result rr)\n        : journeys_{*rr.journeys_},\n          interval_{rr.interval_},\n          search_stats_{rr.search_stats_},\n          algo_stats_{std::move(rr.algo_stats_)} {}\n\n    nigiri::pareto_set<nigiri::routing::journey> journeys_{};\n    nigiri::interval<nigiri::unixtime_t> interval_{};\n    nigiri::routing::search_stats search_stats_{};\n    std::map<std::string, std::uint64_t> algo_stats_{};\n  };\n\nprivate:\n  nigiri::routing::query get_base_query(\n      nigiri::interval<nigiri::unixtime_t> const&) const;\n  std::vector<routing_result> search_interval(\n      std::vector<nigiri::routing::query> const&) const;\n\n  ep::routing const& r_;\n  api::plan_params const& query_;\n  std::vector<api::ModeEnum> const& pre_transit_modes_;\n  std::vector<api::ModeEnum> const& post_transit_modes_;\n  std::vector<api::ModeEnum> const& direct_modes_;\n  std::variant<osr::location, tt_location> const& from_;\n  std::variant<osr::location, tt_location> const& to_;\n  api::Place const& from_place_;\n  api::Place const& to_place_;\n  nigiri::routing::query const& start_time_;\n  std::vector<api::Itinerary>& direct_;\n  nigiri::duration_t fastest_direct_;\n  bool odm_pre_transit_;\n  bool odm_post_transit_;\n  bool odm_direct_;\n  bool ride_sharing_pre_transit_;\n  bool ride_sharing_post_transit_;\n  bool ride_sharing_direct_;\n  unsigned api_version_;\n\n  nigiri::timetable const* tt_;\n  std::shared_ptr<rt> const rt_;\n  nigiri::rt_timetable const* rtt_;\n  motis::elevators const* e_;\n  gbfs::gbfs_routing_data gbfs_rd_;\n  std::variant<osr::location, tt_location> const& start_;\n  std::variant<osr::location, tt_location> const& dest_;\n  std::vector<api::ModeEnum> start_modes_;\n  std::vector<api::ModeEnum> dest_modes_;\n\n  std::optional<std::vector<api::RentalFormFactorEnum>> const&\n      start_form_factors_;\n  std::optional<std::vector<api::RentalFormFactorEnum>> const&\n      dest_form_factors_;\n  std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n      start_propulsion_types_;\n  std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n      dest_propulsion_types_;\n  std::optional<std::vector<std::string>> const& start_rental_providers_;\n  std::optional<std::vector<std::string>> const& dest_rental_providers_;\n  std::optional<std::vector<std::string>> const& start_rental_provider_groups_;\n  std::optional<std::vector<std::string>> const& dest_rental_provider_groups_;\n  bool start_ignore_rental_return_constraints_{};\n  bool dest_ignore_rental_return_constraints_{};\n};\n\n}  // namespace motis::odm\n"
  },
  {
    "path": "include/motis/odm/odm.h",
    "content": "#pragma once\n\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/routing/start_times.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis::odm {\n\nconstexpr auto const kODMTransferBuffer = nigiri::duration_t{5};\nconstexpr auto const kWalkTransportModeId =\n    static_cast<nigiri::transport_mode_id_t>(api::ModeEnum::WALK);\n\nbool by_stop(nigiri::routing::start const&, nigiri::routing::start const&);\n\nenum which_mile { kFirstMile, kLastMile };\n\nbool is_odm_leg(nigiri::routing::journey::leg const&,\n                nigiri::transport_mode_id_t);\n\nbool uses_odm(nigiri::routing::journey const&, nigiri::transport_mode_id_t);\n\nbool is_pure_pt(nigiri::routing::journey const&);\n\nbool is_direct_odm(nigiri::routing::journey const&);\n\nnigiri::duration_t odm_time(nigiri::routing::journey::leg const&);\n\nnigiri::duration_t odm_time(nigiri::routing::journey const&);\n\nnigiri::duration_t pt_time(nigiri::routing::journey const&);\n\nnigiri::duration_t duration(nigiri::routing::start const&);\n\nstd::string odm_label(nigiri::routing::journey const&);\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/odm/prima.h",
    "content": "#pragma once\n\n#include <chrono>\n#include <vector>\n\n#include \"geo/latlng.h\"\n\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/routing/start_times.h\"\n\n#include \"motis-api/motis-api.h\"\n\n#include \"motis/fwd.h\"\n#include \"motis/place.h\"\n\nnamespace motis::ep {\nstruct routing;\n}  // namespace motis::ep\n\nnamespace motis::odm {\n\nconstexpr auto kODMDirectPeriod = std::chrono::seconds{300};\nconstexpr auto kODMDirectFactor = 1.0;\nconstexpr auto kODMOffsetMinImprovement = std::chrono::seconds{60};\nconstexpr auto kODMMaxDuration = std::chrono::seconds{3600};\nconstexpr auto kBlacklistPath = \"/api/blacklist\";\nconstexpr auto kWhitelistPath = \"/api/whitelist\";\nconstexpr auto kRidesharingPath = \"/api/whitelistRideShare\";\nconstexpr auto kInfeasible = std::numeric_limits<nigiri::unixtime_t>::min();\nstatic auto const kReqHeaders = std::map<std::string, std::string>{\n    {\"Content-Type\", \"application/json\"}, {\"Accept\", \"application/json\"}};\n\nusing service_times_t = std::vector<nigiri::interval<nigiri::unixtime_t>>;\n\nstruct direct_ride {\n  nigiri::unixtime_t dep_;\n  nigiri::unixtime_t arr_;\n};\n\nstruct capacities {\n  std::int64_t wheelchairs_;\n  std::int64_t bikes_;\n  std::int64_t passengers_;\n  std::int64_t luggage_;\n};\n\nvoid tag_invoke(boost::json::value_from_tag const&,\n                boost::json::value&,\n                capacities const&);\n\nstruct prima {\n\n  prima(std::string const& prima_url,\n        osr::location const& from,\n        osr::location const& to,\n        api::plan_params const& query);\n\n  void init(nigiri::interval<nigiri::unixtime_t> const& search_intvl,\n            nigiri::interval<nigiri::unixtime_t> const& taxi_intvl,\n            bool use_first_mile_taxi,\n            bool use_last_mile_taxi,\n            bool use_direct_taxi,\n            bool use_first_mile_ride_sharing,\n            bool use_last_mile_ride_sharing,\n            bool use_direct_ride_sharing,\n            nigiri::timetable const& tt,\n            nigiri::rt_timetable const* rtt,\n            ep::routing const& r,\n            elevators const* e,\n            gbfs::gbfs_routing_data& gbfs,\n            api::Place const& from,\n            api::Place const& to,\n            api::plan_params const& query,\n            nigiri::routing::query const& n_query,\n            unsigned api_version);\n\n  std::size_t n_ride_sharing_events() const;\n\n  std::string make_blacklist_taxi_request(\n      nigiri::timetable const&,\n      nigiri::interval<nigiri::unixtime_t> const&) const;\n  bool consume_blacklist_taxi_response(std::string_view json);\n  bool blacklist_taxi(nigiri::timetable const&,\n                      nigiri::interval<nigiri::unixtime_t> const&);\n\n  std::string make_whitelist_taxi_request(\n      std::vector<nigiri::routing::start> const& first_mile,\n      std::vector<nigiri::routing::start> const& last_mile,\n      nigiri::timetable const&) const;\n  bool consume_whitelist_taxi_response(\n      std::string_view json,\n      std::vector<nigiri::routing::journey>&,\n      std::vector<nigiri::routing::start>& first_mile_taxi_rides,\n      std::vector<nigiri::routing::start>& last_mile_taxi_rides);\n  bool whitelist_taxi(std::vector<nigiri::routing::journey>&,\n                      nigiri::timetable const&);\n\n  std::string make_ride_sharing_request(nigiri::timetable const&) const;\n  bool consume_ride_sharing_response(std::string_view json);\n  bool whitelist_ride_sharing(nigiri::timetable const&);\n\n  void extract_taxis_for_persisting(\n      std::vector<nigiri::routing::journey> const& journeys);\n\n  api::plan_params const& query_;\n\n  boost::urls::url taxi_blacklist_;\n  boost::urls::url taxi_whitelist_;\n  boost::urls::url ride_sharing_whitelist_;\n\n  osr::location const from_;\n  osr::location const to_;\n  nigiri::event_type fixed_;\n  capacities cap_;\n\n  std::optional<std::chrono::seconds> direct_duration_;\n\n  std::vector<nigiri::routing::offset> first_mile_taxi_{};\n  std::vector<nigiri::routing::offset> last_mile_taxi_{};\n  std::vector<service_times_t> first_mile_taxi_times_{};\n  std::vector<service_times_t> last_mile_taxi_times_{};\n  std::vector<direct_ride> direct_taxi_{};\n\n  std::vector<nigiri::routing::start> first_mile_ride_sharing_{};\n  nigiri::vecvec<size_t, char> first_mile_ride_sharing_tour_ids_{};\n  std::vector<nigiri::routing::start> last_mile_ride_sharing_{};\n  nigiri::vecvec<size_t, char> last_mile_ride_sharing_tour_ids_{};\n  std::vector<direct_ride> direct_ride_sharing_{};\n  nigiri::vecvec<size_t, char> direct_ride_sharing_tour_ids_{};\n\n  std::vector<nigiri::location_idx_t> whitelist_first_mile_locations_;\n  std::vector<nigiri::location_idx_t> whitelist_last_mile_locations_;\n\n  boost::json::object whitelist_response_;\n};\n\nvoid extract_taxis(std::vector<nigiri::routing::journey> const&,\n                   std::vector<nigiri::routing::start>& first_mile_taxi_rides,\n                   std::vector<nigiri::routing::start>& last_mile_taxi_rides);\n\nvoid fix_first_mile_duration(\n    std::vector<nigiri::routing::journey>& journeys,\n    std::vector<nigiri::routing::start> const& first_mile,\n    std::vector<nigiri::routing::start> const& prev_first_mile,\n    nigiri::transport_mode_id_t mode);\n\nvoid fix_last_mile_duration(\n    std::vector<nigiri::routing::journey>& journeys,\n    std::vector<nigiri::routing::start> const& last_mile,\n    std::vector<nigiri::routing::start> const& prev_last_mile,\n    nigiri::transport_mode_id_t mode);\n\nstd::int64_t to_millis(nigiri::unixtime_t);\n\nnigiri::unixtime_t to_unix(std::int64_t);\n\nstd::size_t n_rides_in_response(boost::json::array const&);\n\nstd::string make_whitelist_request(\n    osr::location const& from,\n    osr::location const& to,\n    std::vector<nigiri::routing::start> const& first_mile,\n    std::vector<nigiri::routing::start> const& last_mile,\n    std::vector<direct_ride> const& direct,\n    nigiri::event_type fixed,\n    capacities const&,\n    nigiri::timetable const&);\n\nvoid add_direct_odm(std::vector<direct_ride> const&,\n                    std::vector<nigiri::routing::journey>&,\n                    place_t const& from,\n                    place_t const& to,\n                    bool arrive_by,\n                    nigiri::transport_mode_id_t);\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/odm/query_factory.h",
    "content": "#pragma once\n\n#include <vector>\n\n#include \"nigiri/routing/query.h\"\n\nnamespace motis::odm {\n\nstruct query_factory {\n  static constexpr auto const kMaxSubQueries = 9U;\n\n  std::vector<nigiri::routing::query> make_queries(\n      bool with_taxi, bool with_ride_sharing) const;\n\nprivate:\n  nigiri::routing::query make(\n      std::vector<nigiri::routing::offset> const& start,\n      nigiri::hash_map<nigiri::location_idx_t,\n                       std::vector<nigiri::routing::td_offset>> const& td_start,\n      std::vector<nigiri::routing::offset> const& dest,\n      nigiri::hash_map<nigiri::location_idx_t,\n                       std::vector<nigiri::routing::td_offset>> const& td_dest)\n      const;\n\npublic:\n  // invariants\n  nigiri::routing::query base_query_;\n\n  // offsets\n  std::vector<nigiri::routing::offset> start_walk_;\n  std::vector<nigiri::routing::offset> dest_walk_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      td_start_walk_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      td_dest_walk_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      start_taxi_short_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      start_taxi_long_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      dest_taxi_short_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      dest_taxi_long_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      start_ride_sharing_;\n  nigiri::hash_map<nigiri::location_idx_t,\n                   std::vector<nigiri::routing::td_offset>>\n      dest_ride_sharing_;\n};\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/odm/shorten.h",
    "content": "#pragma once\n\nnamespace motis::odm {\n\nvoid shorten(std::vector<nigiri::routing::journey>& odm_journeys,\n             std::vector<nigiri::routing::offset> const& first_mile_taxi,\n             std::vector<service_times_t> const& first_mile_taxi_times,\n             std::vector<nigiri::routing::offset> const& last_mile_taxi,\n             std::vector<service_times_t> const& last_mile_taxi_times,\n             nigiri::timetable const&,\n             nigiri::rt_timetable const*,\n             api::plan_params const&);\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/odm/td_offsets.h",
    "content": "#pragma once\n\n#include \"nigiri/routing/query.h\"\n#include \"nigiri/routing/start_times.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/odm/prima.h\"\n\nnamespace motis::odm {\n\nnigiri::routing::td_offsets_t get_td_offsets(\n    auto const& rides, nigiri::transport_mode_id_t const mode) {\n  using namespace std::chrono_literals;\n\n  auto td_offsets = nigiri::routing::td_offsets_t{};\n  utl::equal_ranges_linear(\n      rides, [](auto const& a, auto const& b) { return a.stop_ == b.stop_; },\n      [&](auto&& from_it, auto&& to_it) {\n        td_offsets.emplace(from_it->stop_,\n                           std::vector<nigiri::routing::td_offset>{});\n\n        for (auto const& r : nigiri::it_range{from_it, to_it}) {\n          auto const tdo = nigiri::routing::td_offset{\n              .valid_from_ = std::min(r.time_at_stop_, r.time_at_start_),\n              .duration_ = std::chrono::abs(r.time_at_stop_ - r.time_at_start_),\n              .transport_mode_id_ = mode};\n          auto i = std::lower_bound(begin(td_offsets.at(r.stop_)),\n                                    end(td_offsets.at(r.stop_)), tdo,\n                                    [](auto const& a, auto const& b) {\n                                      return a.valid_from_ < b.valid_from_;\n                                    });\n\n          if (i == end(td_offsets.at(r.stop_)) ||\n              tdo.valid_from_ < i->valid_from_) {\n            i = td_offsets.at(r.stop_).insert(i, tdo);\n          } else if (tdo.duration_ < i->duration_) {\n            *i = tdo;\n          }\n\n          if (i + 1 == end(td_offsets.at(r.stop_)) ||\n              (i + 1)->valid_from_ != tdo.valid_from_ + 1min) {\n            td_offsets.at(r.stop_).insert(\n                i + 1, {.valid_from_ = tdo.valid_from_ + 1min,\n                        .duration_ = nigiri::footpath::kMaxDuration,\n                        .transport_mode_id_ = mode});\n          }\n        }\n      });\n\n  for (auto& [l, tdos] : td_offsets) {\n    for (auto i = begin(tdos); i != end(tdos);) {\n      if (begin(tdos) < i && (i - 1)->duration_ == i->duration_ &&\n          (i - 1)->transport_mode_id_ == i->transport_mode_id_) {\n        i = tdos.erase(i);\n      } else {\n        ++i;\n      }\n    }\n  }\n\n  return td_offsets;\n}\n\nstd::pair<nigiri::routing::td_offsets_t, nigiri::routing::td_offsets_t>\nget_td_offsets_split(std::vector<nigiri::routing::offset> const&,\n                     std::vector<service_times_t> const&,\n                     nigiri::transport_mode_id_t);\n\n}  // namespace motis::odm"
  },
  {
    "path": "include/motis/osr/max_distance.h",
    "content": "#pragma once\n\n#include <chrono>\n\n#include \"osr/routing/profile.h\"\n\nnamespace motis {\n\ndouble get_max_distance(osr::search_profile, std::chrono::seconds);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/osr/mode_to_profile.h",
    "content": "#pragma once\n\n#include \"osr/routing/mode.h\"\n#include \"osr/routing/profile.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis {\n\napi::ModeEnum to_mode(osr::mode);\nosr::search_profile to_profile(api::ModeEnum,\n                               api::PedestrianProfileEnum,\n                               api::ElevationCostsEnum);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/osr/parameters.h",
    "content": "#pragma once\n\n#include \"osr/routing/parameters.h\"\n#include \"osr/routing/profile.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis {\n\nstruct osr_parameters {\n  constexpr static auto const kFootSpeed = 1.2F;\n  constexpr static auto const kWheelchairSpeed = 0.8F;\n  constexpr static auto const kBikeSpeed = 4.2F;\n\n  float const pedestrian_speed_{kFootSpeed};\n  float const cycling_speed_{kBikeSpeed};\n  bool const use_wheelchair_{false};\n};\n\nosr_parameters get_osr_parameters(api::plan_params const&);\n\nosr_parameters get_osr_parameters(api::oneToAll_params const&);\n\nosr_parameters get_osr_parameters(api::oneToMany_params const&);\n\nosr_parameters get_osr_parameters(api::OneToManyParams const&);\n\nosr_parameters get_osr_parameters(api::oneToManyIntermodal_params const&);\n\nosr_parameters get_osr_parameters(api::OneToManyIntermodalParams const&);\n\nosr::profile_parameters to_profile_parameters(osr::search_profile,\n                                              osr_parameters const&);\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/osr/street_routing.h",
    "content": "#pragma once\n\n#include <optional>\n\n#include \"osr/location.h\"\n#include \"osr/routing/profile.h\"\n#include \"osr/routing/route.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/types.h\"\n\nnamespace motis {\n\nusing transport_mode_t = std::uint32_t;\n\nstruct output {\n  output() = default;\n  virtual ~output() = default;\n  output(output const&) = default;\n  output(output&&) = default;\n  output& operator=(output const&) = default;\n  output& operator=(output&&) = default;\n\n  virtual api::ModeEnum get_mode() const = 0;\n  virtual osr::search_profile get_profile() const = 0;\n  virtual bool is_time_dependent() const = 0;\n  virtual transport_mode_t get_cache_key() const = 0;\n  virtual osr::sharing_data const* get_sharing_data() const = 0;\n  virtual void annotate_leg(nigiri::lang_t const&,\n                            osr::node_idx_t from_node,\n                            osr::node_idx_t to_node,\n                            api::Leg&) const = 0;\n  virtual api::Place get_place(nigiri::lang_t const&,\n                               osr::node_idx_t,\n                               std::optional<std::string> const& tz) const = 0;\n};\n\nstruct default_output final : public output {\n  default_output(osr::ways const&, osr::search_profile);\n  default_output(osr::ways const&, nigiri::transport_mode_id_t);\n  ~default_output() override;\n\n  bool is_time_dependent() const override;\n  api::ModeEnum get_mode() const override;\n  osr::search_profile get_profile() const override;\n  transport_mode_t get_cache_key() const override;\n  osr::sharing_data const* get_sharing_data() const override;\n  void annotate_leg(nigiri::lang_t const&,\n                    osr::node_idx_t,\n                    osr::node_idx_t,\n                    api::Leg&) const override;\n  api::Place get_place(nigiri::lang_t const&,\n                       osr::node_idx_t,\n                       std::optional<std::string> const& tz) const override;\n\n  osr::ways const& w_;\n  osr::search_profile profile_;\n  nigiri::transport_mode_id_t id_;\n};\n\nusing street_routing_cache_key_t = std::\n    tuple<osr::location, osr::location, transport_mode_t, nigiri::unixtime_t>;\n\nusing street_routing_cache_t =\n    hash_map<street_routing_cache_key_t, std::optional<osr::path>>;\n\napi::Itinerary dummy_itinerary(api::Place const& from,\n                               api::Place const& to,\n                               api::ModeEnum,\n                               nigiri::unixtime_t const start_time,\n                               nigiri::unixtime_t const end_time);\n\napi::Itinerary street_routing(osr::ways const&,\n                              osr::lookup const&,\n                              elevators const*,\n                              osr::elevation_storage const*,\n                              nigiri::lang_t const& lang,\n                              api::Place const& from,\n                              api::Place const& to,\n                              output const&,\n                              std::optional<nigiri::unixtime_t> start_time,\n                              std::optional<nigiri::unixtime_t> end_time,\n                              double max_matching_distance,\n                              osr_parameters const&,\n                              street_routing_cache_t&,\n                              osr::bitvec<osr::node_idx_t>& blocked_mem,\n                              unsigned api_version,\n                              bool detailed_leg = true,\n                              std::chrono::seconds max = std::chrono::seconds{\n                                  3600});\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/parse_location.h",
    "content": "#pragma once\n\n#include <optional>\n#include <string>\n#include <string_view>\n\n#include \"osr/routing/route.h\"\n\n#include \"nigiri/routing/query.h\"\n#include \"nigiri/types.h\"\n\nnamespace motis {\n\nstd::optional<osr::location> parse_location(std::string_view,\n                                            char separator = ',');\n\ndate::sys_days parse_iso_date(std::string_view);\n\nnigiri::routing::query cursor_to_query(std::string_view);\n\nstd::pair<nigiri::direction, nigiri::unixtime_t> parse_cursor(std::string_view);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/place.h",
    "content": "#pragma once\n\n#include \"osr/location.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis {\n\nstruct tt_location {\n  explicit tt_location(nigiri::rt::run_stop const& stop);\n  explicit tt_location(\n      nigiri::location_idx_t l,\n      nigiri::location_idx_t scheduled = nigiri::location_idx_t::invalid());\n\n  friend std::ostream& operator<<(std::ostream& out, tt_location const& l) {\n    return out << \"{ l=\" << l.l_ << \", scheduled=\" << l.scheduled_ << \" }\";\n  }\n\n  nigiri::location_idx_t l_;\n  nigiri::location_idx_t scheduled_;\n};\n\nusing place_t = std::variant<osr::location, tt_location>;\n\ninline std::ostream& operator<<(std::ostream& out, place_t const p) {\n  return std::visit([&](auto const l) -> std::ostream& { return out << l; }, p);\n}\n\nosr::level_t get_lvl(osr::ways const*,\n                     osr::platforms const*,\n                     platform_matches_t const*,\n                     nigiri::location_idx_t);\n\napi::Place to_place(osr::location,\n                    std::string_view name,\n                    std::optional<std::string> const& tz);\n\napi::Place to_place(\n    nigiri::timetable const*,\n    tag_lookup const*,\n    osr::ways const*,\n    osr::platforms const*,\n    platform_matches_t const*,\n    adr_ext const*,\n    tz_map_t const*,\n    nigiri::lang_t const&,\n    place_t,\n    place_t start = osr::location{},\n    place_t dest = osr::location{},\n    std::string_view name = \"\",\n    std::optional<std::string> const& fallback_tz = std::nullopt);\n\napi::Place to_place(nigiri::timetable const*,\n                    tag_lookup const*,\n                    osr::ways const*,\n                    osr::platforms const*,\n                    platform_matches_t const*,\n                    adr_ext const* ae,\n                    tz_map_t const* tz,\n                    nigiri::lang_t const&,\n                    nigiri::rt::run_stop const&,\n                    place_t start = osr::location{},\n                    place_t dest = osr::location{});\n\nosr::location get_location(api::Place const&);\n\nosr::location get_location(nigiri::timetable const*,\n                           osr::ways const*,\n                           osr::platforms const*,\n                           platform_matches_t const*,\n                           place_t const loc,\n                           place_t const start = {},\n                           place_t const dest = {});\n\nplace_t get_place(nigiri::timetable const*,\n                  tag_lookup const*,\n                  std::string_view user_input);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/point_rtree.h",
    "content": "#pragma once\n\n#include <array>\n\n#include \"cista/strong.h\"\n\n#include \"rtree.h\"\n\n#include \"geo/box.h\"\n#include \"geo/latlng.h\"\n\nnamespace motis {\n\ntemplate <typename T, typename Fn>\nconcept RtreePosHandler = requires(geo::latlng const& pos, T const x, Fn&& f) {\n  { std::forward<Fn>(f)(pos, x) };\n};\n\ntemplate <typename T>\nstruct point_rtree {\n  point_rtree() : rtree_{rtree_new()} {}\n\n  ~point_rtree() {\n    if (rtree_ != nullptr) {\n      rtree_free(rtree_);\n    }\n  }\n\n  point_rtree(point_rtree const& o) {\n    if (this != &o) {\n      if (rtree_ != nullptr) {\n        rtree_free(rtree_);\n      }\n      rtree_ = rtree_clone(o.rtree_);\n    }\n  }\n\n  point_rtree(point_rtree&& o) {\n    if (this != &o) {\n      rtree_ = o.rtree_;\n      o.rtree_ = nullptr;\n    }\n  }\n\n  point_rtree& operator=(point_rtree const& o) {\n    if (this != &o) {\n      if (rtree_ != nullptr) {\n        rtree_free(rtree_);\n      }\n      rtree_ = rtree_clone(o.rtree_);\n    }\n    return *this;\n  }\n\n  point_rtree& operator=(point_rtree&& o) {\n    if (this != &o) {\n      rtree_ = o.rtree_;\n      o.rtree_ = nullptr;\n    }\n    return *this;\n  }\n\n  void add(geo::latlng const& pos, T const t) {\n    auto const min_corner = std::array{pos.lng(), pos.lat()};\n    rtree_insert(\n        rtree_, min_corner.data(), nullptr,\n        reinterpret_cast<void*>(static_cast<std::size_t>(cista::to_idx(t))));\n  }\n\n  void remove(geo::latlng const& pos, T const t) {\n    auto const min_corner = std::array{pos.lng(), pos.lat()};\n    rtree_delete(\n        rtree_, min_corner.data(), nullptr,\n        reinterpret_cast<void*>(static_cast<std::size_t>(cista::to_idx(t))));\n  }\n\n  std::vector<T> in_radius(geo::latlng const& x, double distance) const {\n    auto ret = std::vector<T>{};\n    in_radius(x, distance, [&](auto&& item) { ret.emplace_back(item); });\n    return ret;\n  }\n\n  template <typename Fn>\n  void in_radius(geo::latlng const& x, double distance, Fn&& fn) const {\n    find(geo::box{x, distance}, [&](geo::latlng const& pos, T const item) {\n      if (geo::distance(x, pos) < distance) {\n        fn(item);\n      }\n    });\n  }\n\n  template <typename Fn>\n  void find(geo::box const& b, Fn&& fn) const {\n    auto const min = b.min_.lnglat();\n    auto const max = b.max_.lnglat();\n    rtree_search(\n        rtree_, min.data(), max.data(),\n        [](double const* pos, double const* /* max */, void const* item,\n           void* udata) {\n          if constexpr (RtreePosHandler<T, Fn>) {\n            (*reinterpret_cast<Fn*>(udata))(\n                geo::latlng{pos[1], pos[0]},\n                T{static_cast<cista::base_t<T>>(\n                    reinterpret_cast<std::size_t>(item))});\n          } else {\n            (*reinterpret_cast<Fn*>(udata))(T{static_cast<cista::base_t<T>>(\n                reinterpret_cast<std::size_t>(item))});\n          }\n          return true;\n        },\n        &fn);\n  }\n\n  rtree* rtree_{nullptr};\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/polyline.h",
    "content": "#pragma once\n\n#include \"geo/polyline.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis {\n\ntemplate <std::int64_t Precision>\napi::EncodedPolyline to_polyline(geo::polyline const& polyline);\n\napi::EncodedPolyline empty_polyline();\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/railviz.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n\nnamespace motis {\n\nstruct railviz_static_index {\n  railviz_static_index(nigiri::timetable const&, nigiri::shapes_storage const*);\n  ~railviz_static_index();\n\n  struct impl;\n  std::unique_ptr<impl> impl_;\n};\n\nstruct railviz_rt_index {\n  railviz_rt_index(nigiri::timetable const&, nigiri::rt_timetable const&);\n  ~railviz_rt_index();\n\n  struct impl;\n  std::unique_ptr<impl> impl_;\n};\n\napi::trips_response get_trains(tag_lookup const&,\n                               nigiri::timetable const&,\n                               nigiri::rt_timetable const*,\n                               nigiri::shapes_storage const*,\n                               osr::ways const*,\n                               osr::platforms const*,\n                               platform_matches_t const*,\n                               adr_ext const*,\n                               tz_map_t const*,\n                               railviz_static_index::impl const&,\n                               railviz_rt_index::impl const&,\n                               api::trips_params const&,\n                               unsigned api_version);\n\napi::routes_response get_routes(tag_lookup const&,\n                                nigiri::timetable const&,\n                                nigiri::rt_timetable const*,\n                                nigiri::shapes_storage const*,\n                                osr::ways const*,\n                                osr::platforms const*,\n                                platform_matches_t const*,\n                                adr_ext const*,\n                                tz_map_t const*,\n                                railviz_static_index::impl const&,\n                                railviz_rt_index::impl const&,\n                                api::routes_params const&,\n                                unsigned api_version);\n\napi::routeDetails_response get_route_details(tag_lookup const&,\n                                             nigiri::timetable const&,\n                                             nigiri::rt_timetable const*,\n                                             nigiri::shapes_storage const*,\n                                             osr::ways const*,\n                                             osr::platforms const*,\n                                             platform_matches_t const*,\n                                             adr_ext const*,\n                                             tz_map_t const*,\n                                             railviz_static_index::impl const&,\n                                             railviz_rt_index::impl const&,\n                                             api::routeDetails_params const&,\n                                             unsigned api_version);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/route_shapes.h",
    "content": "#pragma once\n\n#include <chrono>\n#include <filesystem>\n#include <optional>\n\n#include \"boost/json/fwd.hpp\"\n\n#include \"cista/containers/pair.h\"\n#include \"cista/containers/vector.h\"\n\n#include \"lmdb/lmdb.hpp\"\n\n#include \"geo/box.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"osr/routing/profile.h\"\n\n#include \"motis/config.h\"\n#include \"motis/fwd.h\"\n\nnamespace motis {\n\nstruct shape_cache_entry {\n  bool valid() const {\n    return shape_idx_ != nigiri::scoped_shape_idx_t::invalid();\n  }\n\n  nigiri::scoped_shape_idx_t shape_idx_{nigiri::scoped_shape_idx_t::invalid()};\n  cista::offset::vector<nigiri::shape_offset_t> offsets_;\n  geo::box route_bbox_;\n  cista::offset::vector<geo::box> segment_bboxes_;\n};\n\nusing shape_cache_key = cista::offset::pair<osr::search_profile,\n                                            cista::offset::vector<geo::latlng>>;\n\nstruct shape_cache {\n  explicit shape_cache(std::filesystem::path const&,\n                       mdb_size_t = sizeof(void*) >= 8\n                                        ? 256ULL * 1024ULL * 1024ULL * 1024ULL\n                                        : 256U * 1024U * 1024U);\n  ~shape_cache();\n\n  std::optional<shape_cache_entry> get(shape_cache_key const&);\n  void put(shape_cache_key const&, shape_cache_entry const&);\n  void sync();\n\n  lmdb::env env_;\n  std::chrono::time_point<std::chrono::steady_clock> last_sync_;\n};\n\nboost::json::object route_shape_debug(osr::ways const&,\n                                      osr::lookup const&,\n                                      nigiri::timetable const&,\n                                      nigiri::route_idx_t);\n\nvoid route_shapes(osr::ways const&,\n                  osr::lookup const&,\n                  nigiri::timetable const&,\n                  nigiri::shapes_storage&,\n                  config::timetable::route_shapes const&,\n                  std::array<bool, nigiri::kNumClasses> const&,\n                  shape_cache*);\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/rt/auser.h",
    "content": "#pragma once\n\n#include <string>\n\n#include \"nigiri/rt/vdv_aus.h\"\n\nnamespace motis {\n\nstruct auser {\n  auser(nigiri::timetable const&,\n        nigiri::source_idx_t,\n        nigiri::rt::vdv_aus::updater::xml_format);\n  std::string fetch_url(std::string_view base_url);\n  nigiri::rt::vdv_aus::statistics consume_update(std::string const&,\n                                                 nigiri::rt_timetable&,\n                                                 bool inplace = false);\n\n  std::chrono::nanoseconds::rep update_state_{0};\n  nigiri::rt::vdv_aus::updater upd_;\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/rt/rt_metrics.h",
    "content": "#pragma once\n\n#include \"prometheus/counter.h\"\n#include \"prometheus/family.h\"\n#include \"prometheus/gauge.h\"\n\n#include \"motis/metrics_registry.h\"\n\nnamespace motis {\n\nstruct rt_metric_families {\n  explicit rt_metric_families(prometheus::Registry& registry)\n      : gtfsrt_updates_requested_{prometheus::BuildCounter()\n                                      .Name(\"nigiri_gtfsrt_updates_requested_\"\n                                            \"total\")\n                                      .Help(\"Number of update attempts of the \"\n                                            \"GTFS-RT feed\")\n                                      .Register(registry)},\n        gtfsrt_updates_successful_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_updates_successful_total\")\n                .Help(\"Number of successful updates of the GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_updates_error_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_updates_error_total\")\n                .Help(\"Number of failed updates of the GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_total_entities_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_total_entities_total\")\n                .Help(\"Total number of entities in the GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_total_entities_success_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_total_entities_success_total\")\n                .Help(\"Number of entities in the GTFS-RT feed that were \"\n                      \"successfully processed\")\n                .Register(registry)},\n        gtfsrt_total_entities_fail_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_total_entities_fail_total\")\n                .Help(\"Number of entities in the GTFS-RT feed that could not \"\n                      \"be processed\")\n                .Register(registry)},\n        gtfsrt_unsupported_deleted_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_unsupported_deleted_total\")\n                .Help(\"Number of unsupported deleted entities in the GTFS-RT \"\n                      \"feed\")\n                .Register(registry)},\n        gtfsrt_unsupported_vehicle_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_unsupported_vehicle_total\")\n                .Help(\"Number of unsupported vehicle entities in the GTFS-RT \"\n                      \"feed\")\n                .Register(registry)},\n        gtfsrt_unsupported_alert_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_unsupported_alert_total\")\n                .Help(\n                    \"Number of unsupported alert entities in the GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_unsupported_no_trip_id_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_unsupported_no_trip_id_total\")\n                .Help(\"Number of unsupported trips without trip id in the \"\n                      \"GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_no_trip_update_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_no_trip_update_total\")\n                .Help(\"Number of unsupported trips without trip update in the \"\n                      \"GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_trip_update_without_trip_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_trip_update_without_trip_total\")\n                .Help(\"Number of unsupported trip updates without trip in the \"\n                      \"GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_trip_resolve_error_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_trip_resolve_error_total\")\n                .Help(\"Number of unresolved trips in the GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_unsupported_schedule_relationship_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_gtfsrt_unsupported_schedule_relationship_total\")\n                .Help(\"Number of unsupported schedule relationships in the \"\n                      \"GTFS-RT feed\")\n                .Register(registry)},\n        gtfsrt_feed_timestamp_{prometheus::BuildGauge()\n                                   .Name(\"nigiri_gtfsrt_feed_timestamp_seconds\")\n                                   .Help(\"Timestamp of the GTFS-RT feed\")\n                                   .Register(registry)},\n        gtfsrt_last_update_timestamp_{\n            prometheus::BuildGauge()\n                .Name(\"nigiri_gtfsrt_last_update_timestamp_seconds\")\n                .Help(\"Last update timestamp of the GTFS-RT feed\")\n                .Register(registry)},\n        vdvaus_updates_requested_{prometheus::BuildCounter()\n                                      .Name(\"nigiri_vdvaus_updates_requested_\"\n                                            \"total\")\n                                      .Help(\"Number of update attempts of the \"\n                                            \"VDV AUS feed\")\n                                      .Register(registry)},\n        vdvaus_updates_successful_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_updates_successful_total\")\n                .Help(\"Number of successful updates of the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_updates_error_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_updates_error_total\")\n                .Help(\"Number of failed updates of the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_unsupported_additional_runs_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_unsupported_additional_runs_total\")\n                .Help(\"Number of unsupported additional runs in the VDV AUS \"\n                      \"feed\")\n                .Register(registry)},\n        vdvaus_unsupported_additional_stops_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_unsupported_additional_runs_total\")\n                .Help(\"Number of additional stops in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_current_matches_total_{\n            prometheus::BuildGauge()\n                .Name(\"nigiri_vdvaus_current_matches_total\")\n                .Help(\"Current number of unique run IDs for which matching \"\n                      \"was performed\")\n                .Register(registry)},\n        vdvaus_current_matches_non_empty_{\n            prometheus::BuildGauge()\n                .Name(\"nigiri_vdvaus_current_matches_non_empty_total\")\n                .Help(\"Current number of unique run IDs for which a matching \"\n                      \"was performed and a non-empty result was achieved\")\n                .Register(registry)},\n        vdvaus_total_runs_{prometheus::BuildCounter()\n                               .Name(\"nigiri_vdvaus_total_runs_total\")\n                               .Help(\"Total number of runs in the VDV AUS feed\")\n                               .Register(registry)},\n        vdvaus_complete_runs_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_complete_runs_total\")\n                .Help(\"Total number of complete runs in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_unique_runs_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_unique_runs_total\")\n                .Help(\"Total number of unique runs in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_match_attempts_{prometheus::BuildCounter()\n                                   .Name(\"nigiri_vdvaus_match_attempts_total\")\n                                   .Help(\"Total number of match attempts\")\n                                   .Register(registry)},\n        vdvaus_matched_runs_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_matched_runs_total\")\n                .Help(\"Number of runs of the VDV AUS feed for which a \"\n                      \"successful match attempt took place\")\n                .Register(registry)},\n        vdvaus_found_runs_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_found_runs_total\")\n                .Help(\"Number of runs of the VDV AUS feed for which a matching \"\n                      \"run in the static timetable could be looked up \"\n                      \"successfully\")\n                .Register(registry)},\n        vdvaus_multiple_matches_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_multiple_matches_total\")\n                .Help(\"Number of times a run of the VDV AUS feed could not be \"\n                      \"matched to a transport in the timetable since there \"\n                      \"were multiple transports with the same score\")\n                .Register(registry)},\n        vdvaus_incomplete_not_seen_before_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_incomplete_not_seen_before_total\")\n                .Help(\n                    \"Number of times an incomplete run was encountered before \"\n                    \"seeing a complete version of it in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_complete_after_incomplete_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_complete_after_incomplete_total\")\n                .Help(\"Number of times a complete run was encountered in the \"\n                      \"feed after seeing an incomplete version before\")\n                .Register(registry)},\n        vdvaus_no_transport_found_at_stop_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_no_transport_found_at_stop_total\")\n                .Help(\"Number of times that no transport could be found at the \"\n                      \"stop specified in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_total_stops_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_total_stops_total\")\n                .Help(\"Total number of stops in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_resolved_stops_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_resolved_stops_total\")\n                .Help(\"Number of stops that could be resolved to locations in \"\n                      \"the timetable\")\n                .Register(registry)},\n        vdvaus_runs_without_stops_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_runs_without_stops_total\")\n                .Help(\"Number of times a run without any stops was encountered \"\n                      \"in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_cancelled_runs_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_cancelled_runs_total\")\n                .Help(\"Number of cancelled runs in the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_skipped_vdv_stops_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_skipped_vdv_stops_total\")\n                .Help(\"Number of stops in the VDV AUS feed that had to be \"\n                      \"skipped while updating a run since they had no \"\n                      \"counterpart in the run of the timetable\")\n                .Register(registry)},\n        vdvaus_excess_vdv_stops_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_excess_vdv_stops_total\")\n                .Help(\n                    \"Number of additional stops at the end of runs in VDV AUS \"\n                    \"feed that had no corresponding stop in the run of the \"\n                    \"timetable that was updated\")\n                .Register(registry)},\n        vdvaus_updated_events_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_updated_events_total\")\n                .Help(\"Number of arrival/departure times \"\n                      \"that were updated by the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_propagated_delays_{\n            prometheus::BuildCounter()\n                .Name(\"nigiri_vdvaus_propagated_delays_total\")\n                .Help(\"Number of delay propagations by the VDV AUS feed\")\n                .Register(registry)},\n        vdvaus_feed_timestamp_{prometheus::BuildGauge()\n                                   .Name(\"nigiri_vdvaus_feed_timestamp_seconds\")\n                                   .Help(\"Timestamp of the VDV AUS feed\")\n                                   .Register(registry)},\n        vdvaus_last_update_timestamp_{\n            prometheus::BuildGauge()\n                .Name(\"nigiri_vdvaus_last_update_timestamp_seconds\")\n                .Help(\"Last update timestamp of the VDV AUS feed\")\n                .Register(registry)} {}\n\n  prometheus::Family<prometheus::Counter>& gtfsrt_updates_requested_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_updates_successful_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_updates_error_;\n\n  prometheus::Family<prometheus::Counter>& gtfsrt_total_entities_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_total_entities_success_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_total_entities_fail_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_unsupported_deleted_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_unsupported_vehicle_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_unsupported_alert_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_unsupported_no_trip_id_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_no_trip_update_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_trip_update_without_trip_;\n  prometheus::Family<prometheus::Counter>& gtfsrt_trip_resolve_error_;\n  prometheus::Family<prometheus::Counter>&\n      gtfsrt_unsupported_schedule_relationship_;\n  prometheus::Family<prometheus::Gauge>& gtfsrt_feed_timestamp_;\n  prometheus::Family<prometheus::Gauge>& gtfsrt_last_update_timestamp_;\n\n  prometheus::Family<prometheus::Counter>& vdvaus_updates_requested_;\n  prometheus::Family<prometheus::Counter>& vdvaus_updates_successful_;\n  prometheus::Family<prometheus::Counter>& vdvaus_updates_error_;\n\n  prometheus::Family<prometheus::Counter>& vdvaus_unsupported_additional_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_unsupported_additional_stops_;\n  prometheus::Family<prometheus::Gauge>& vdvaus_current_matches_total_;\n  prometheus::Family<prometheus::Gauge>& vdvaus_current_matches_non_empty_;\n  prometheus::Family<prometheus::Counter>& vdvaus_total_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_complete_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_unique_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_match_attempts_;\n  prometheus::Family<prometheus::Counter>& vdvaus_matched_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_found_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_multiple_matches_;\n  prometheus::Family<prometheus::Counter>& vdvaus_incomplete_not_seen_before_;\n  prometheus::Family<prometheus::Counter>& vdvaus_complete_after_incomplete_;\n  prometheus::Family<prometheus::Counter>& vdvaus_no_transport_found_at_stop_;\n  prometheus::Family<prometheus::Counter>& vdvaus_total_stops_;\n  prometheus::Family<prometheus::Counter>& vdvaus_resolved_stops_;\n  prometheus::Family<prometheus::Counter>& vdvaus_runs_without_stops_;\n  prometheus::Family<prometheus::Counter>& vdvaus_cancelled_runs_;\n  prometheus::Family<prometheus::Counter>& vdvaus_skipped_vdv_stops_;\n  prometheus::Family<prometheus::Counter>& vdvaus_excess_vdv_stops_;\n  prometheus::Family<prometheus::Counter>& vdvaus_updated_events_;\n  prometheus::Family<prometheus::Counter>& vdvaus_propagated_delays_;\n  prometheus::Family<prometheus::Gauge>& vdvaus_feed_timestamp_;\n  prometheus::Family<prometheus::Gauge>& vdvaus_last_update_timestamp_;\n};\n\nstruct gtfsrt_metrics {\n  explicit gtfsrt_metrics(std::string const& tag, rt_metric_families const& m)\n      : updates_requested_{m.gtfsrt_updates_requested_.Add({{\"tag\", tag}})},\n        updates_successful_{m.gtfsrt_updates_successful_.Add({{\"tag\", tag}})},\n        updates_error_{m.gtfsrt_updates_error_.Add({{\"tag\", tag}})},\n        total_entities_{m.gtfsrt_total_entities_.Add({{\"tag\", tag}})},\n        total_entities_success_{\n            m.gtfsrt_total_entities_success_.Add({{\"tag\", tag}})},\n        total_entities_fail_{m.gtfsrt_total_entities_fail_.Add({{\"tag\", tag}})},\n        unsupported_deleted_{m.gtfsrt_unsupported_deleted_.Add({{\"tag\", tag}})},\n        unsupported_vehicle_{m.gtfsrt_unsupported_vehicle_.Add({{\"tag\", tag}})},\n        unsupported_alert_{m.gtfsrt_unsupported_alert_.Add({{\"tag\", tag}})},\n        unsupported_no_trip_id_{\n            m.gtfsrt_unsupported_no_trip_id_.Add({{\"tag\", tag}})},\n        no_trip_update_{m.gtfsrt_no_trip_update_.Add({{\"tag\", tag}})},\n        trip_update_without_trip_{\n            m.gtfsrt_trip_update_without_trip_.Add({{\"tag\", tag}})},\n        trip_resolve_error_{m.gtfsrt_trip_resolve_error_.Add({{\"tag\", tag}})},\n        unsupported_schedule_relationship_{\n            m.gtfsrt_unsupported_schedule_relationship_.Add({{\"tag\", tag}})},\n        feed_timestamp_{m.gtfsrt_feed_timestamp_.Add({{\"tag\", tag}})},\n        last_update_timestamp_{\n            m.gtfsrt_last_update_timestamp_.Add({{\"tag\", tag}})} {}\n\n  void update(nigiri::rt::statistics const& stats) const {\n    total_entities_.Increment(stats.total_entities_);\n    total_entities_success_.Increment(stats.total_entities_success_);\n    total_entities_fail_.Increment(stats.total_entities_fail_);\n    unsupported_deleted_.Increment(stats.unsupported_deleted_);\n    unsupported_no_trip_id_.Increment(stats.unsupported_no_trip_id_);\n    no_trip_update_.Increment(stats.no_trip_update_);\n    trip_update_without_trip_.Increment(stats.trip_update_without_trip_);\n    trip_resolve_error_.Increment(stats.trip_resolve_error_);\n    unsupported_schedule_relationship_.Increment(\n        stats.unsupported_schedule_relationship_);\n    feed_timestamp_.Set(\n        static_cast<double>(stats.feed_timestamp_.time_since_epoch().count()));\n  }\n\n  prometheus::Counter& updates_requested_;\n  prometheus::Counter& updates_successful_;\n  prometheus::Counter& updates_error_;\n\n  prometheus::Counter& total_entities_;\n  prometheus::Counter& total_entities_success_;\n  prometheus::Counter& total_entities_fail_;\n  prometheus::Counter& unsupported_deleted_;\n  prometheus::Counter& unsupported_vehicle_;\n  prometheus::Counter& unsupported_alert_;\n  prometheus::Counter& unsupported_no_trip_id_;\n  prometheus::Counter& no_trip_update_;\n  prometheus::Counter& trip_update_without_trip_;\n  prometheus::Counter& trip_resolve_error_;\n  prometheus::Counter& unsupported_schedule_relationship_;\n  prometheus::Gauge& feed_timestamp_;\n  prometheus::Gauge& last_update_timestamp_;\n};\n\nstruct vdvaus_metrics {\n  explicit vdvaus_metrics(std::string const& tag, rt_metric_families const& m)\n      : updates_requested_{m.vdvaus_updates_requested_.Add({{\"tag\", tag}})},\n        updates_successful_{m.vdvaus_updates_successful_.Add({{\"tag\", tag}})},\n        updates_error_{m.vdvaus_updates_error_.Add({{\"tag\", tag}})},\n        unsupported_additional_runs_{\n            m.vdvaus_unsupported_additional_runs_.Add({{\"tag\", tag}})},\n        unsupported_additional_stops_{\n            m.vdvaus_unsupported_additional_stops_.Add({{\"tag\", tag}})},\n        current_matches_total_{\n            m.vdvaus_current_matches_total_.Add({{\"tag\", tag}})},\n        current_matches_non_empty_{\n            m.vdvaus_current_matches_non_empty_.Add({{\"tag\", tag}})},\n        total_runs_{m.vdvaus_total_runs_.Add({{\"tag\", tag}})},\n        complete_runs_{m.vdvaus_complete_runs_.Add({{\"tag\", tag}})},\n        unique_runs_{m.vdvaus_unique_runs_.Add({{\"tag\", tag}})},\n        match_attempts_{m.vdvaus_match_attempts_.Add({{\"tag\", tag}})},\n        matched_runs_{m.vdvaus_matched_runs_.Add({{\"tag\", tag}})},\n        found_runs_{m.vdvaus_found_runs_.Add({{\"tag\", tag}})},\n        multiple_matches_{m.vdvaus_multiple_matches_.Add({{\"tag\", tag}})},\n        incomplete_not_seen_before_{\n            m.vdvaus_incomplete_not_seen_before_.Add({{\"tag\", tag}})},\n        complete_after_incomplete_{\n            m.vdvaus_complete_after_incomplete_.Add({{\"tag\", tag}})},\n        no_transport_found_at_stop_{\n            m.vdvaus_no_transport_found_at_stop_.Add({{\"tag\", tag}})},\n        total_stops_{m.vdvaus_total_stops_.Add({{\"tag\", tag}})},\n        resolved_stops_{m.vdvaus_resolved_stops_.Add({{\"tag\", tag}})},\n        runs_without_stops_{m.vdvaus_runs_without_stops_.Add({{\"tag\", tag}})},\n        cancelled_runs_{m.vdvaus_cancelled_runs_.Add({{\"tag\", tag}})},\n        skipped_vdv_stops_{m.vdvaus_skipped_vdv_stops_.Add({{\"tag\", tag}})},\n        excess_vdv_stops_{m.vdvaus_excess_vdv_stops_.Add({{\"tag\", tag}})},\n        updated_events_{m.vdvaus_updated_events_.Add({{\"tag\", tag}})},\n        propagated_delays_{m.vdvaus_propagated_delays_.Add({{\"tag\", tag}})},\n        last_update_timestamp_{\n            m.vdvaus_last_update_timestamp_.Add({{\"tag\", tag}})} {}\n\n  void update(nigiri::rt::vdv_aus::statistics const& stats) const {\n    unsupported_additional_runs_.Increment(stats.unsupported_additional_runs_);\n    unsupported_additional_stops_.Increment(\n        stats.unsupported_additional_stops_);\n    current_matches_total_.Set(\n        static_cast<double>(stats.current_matches_total_));\n    current_matches_non_empty_.Set(stats.current_matches_non_empty_);\n    total_runs_.Increment(stats.total_runs_);\n    complete_runs_.Increment(stats.complete_runs_);\n    unique_runs_.Increment(stats.unique_runs_);\n    match_attempts_.Increment(stats.match_attempts_);\n    matched_runs_.Increment(stats.matched_runs_);\n    found_runs_.Increment(stats.found_runs_);\n    multiple_matches_.Increment(stats.multiple_matches_);\n    incomplete_not_seen_before_.Increment(stats.incomplete_not_seen_before_);\n    complete_after_incomplete_.Increment(stats.complete_after_incomplete_);\n    no_transport_found_at_stop_.Increment(stats.no_transport_found_at_stop_);\n    total_stops_.Increment(stats.total_stops_);\n    resolved_stops_.Increment(stats.resolved_stops_);\n    runs_without_stops_.Increment(stats.runs_without_stops_);\n    cancelled_runs_.Increment(stats.cancelled_runs_);\n    skipped_vdv_stops_.Increment(stats.skipped_vdv_stops_);\n    excess_vdv_stops_.Increment(stats.excess_vdv_stops_);\n    updated_events_.Increment(stats.updated_events_);\n    propagated_delays_.Increment(stats.propagated_delays_);\n  }\n\n  prometheus::Counter& updates_requested_;\n  prometheus::Counter& updates_successful_;\n  prometheus::Counter& updates_error_;\n\n  prometheus::Counter& unsupported_additional_runs_;\n  prometheus::Counter& unsupported_additional_stops_;\n  prometheus::Gauge& current_matches_total_;\n  prometheus::Gauge& current_matches_non_empty_;\n  prometheus::Counter& total_runs_;\n  prometheus::Counter& complete_runs_;\n  prometheus::Counter& unique_runs_;\n  prometheus::Counter& match_attempts_;\n  prometheus::Counter& matched_runs_;\n  prometheus::Counter& found_runs_;\n  prometheus::Counter& multiple_matches_;\n  prometheus::Counter& incomplete_not_seen_before_;\n  prometheus::Counter& complete_after_incomplete_;\n  prometheus::Counter& no_transport_found_at_stop_;\n  prometheus::Counter& total_stops_;\n  prometheus::Counter& resolved_stops_;\n  prometheus::Counter& runs_without_stops_;\n  prometheus::Counter& cancelled_runs_;\n  prometheus::Counter& skipped_vdv_stops_;\n  prometheus::Counter& excess_vdv_stops_;\n  prometheus::Counter& updated_events_;\n  prometheus::Counter& propagated_delays_;\n  prometheus::Gauge& last_update_timestamp_;\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/rt_update.h",
    "content": "#pragma once\n\n#include <chrono>\n#include <memory>\n\n#include \"boost/asio/io_context.hpp\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis {\n\nvoid run_rt_update(boost::asio::io_context&, config const&, data&);\n\n}"
  },
  {
    "path": "include/motis/server.h",
    "content": "#pragma once\n\n#include <string_view>\n\n#include \"boost/url/url_view.hpp\"\n\nnamespace motis {\n\nstruct data;\nstruct config;\n\nint server(data d, config const& c, std::string_view);\n\nunsigned get_api_version(boost::urls::url_view const&);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/tag_lookup.h",
    "content": "#pragma once\n\n#include <filesystem>\n#include <string>\n#include <string_view>\n\n#include \"cista/memory_holder.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/fwd.h\"\n\nnamespace motis {\n\ntemplate <typename T = std::string_view>\nstruct trip_id {\n  T start_date_;\n  T start_time_;\n  T tag_;\n  T trip_id_;\n};\n\ntrip_id<std::string_view> split_trip_id(std::string_view);\n\nstruct tag_lookup {\n  void add(nigiri::source_idx_t, std::string_view str);\n\n  nigiri::source_idx_t get_src(std::string_view tag) const;\n  std::string_view get_tag(nigiri::source_idx_t) const;\n  std::string id(nigiri::timetable const&, nigiri::location_idx_t) const;\n  std::string id(nigiri::timetable const&,\n                 nigiri::rt::run_stop,\n                 nigiri::event_type) const;\n  std::string route_id(nigiri::rt::run_stop, nigiri::event_type) const;\n\n  trip_id<std::string> id_fragments(nigiri::timetable const&,\n                                    nigiri::rt::run_stop,\n                                    nigiri::event_type const) const;\n\n  nigiri::location_idx_t get_location(nigiri::timetable const&,\n                                      std::string_view) const;\n  std::optional<nigiri::location_idx_t> find_location(nigiri::timetable const&,\n                                                      std::string_view) const;\n  std::pair<nigiri::rt::run, nigiri::trip_idx_t> get_trip(\n      nigiri::timetable const&,\n      nigiri::rt_timetable const*,\n      std::string_view) const;\n\n  friend std::ostream& operator<<(std::ostream&, tag_lookup const&);\n  void write(std::filesystem::path const&) const;\n  static cista::wrapped<tag_lookup> read(std::filesystem::path const&);\n\n  nigiri::vecvec<nigiri::source_idx_t, char, std::uint32_t> src_to_tag_;\n  nigiri::hash_map<nigiri::string, nigiri::source_idx_t> tag_to_src_;\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/tiles_data.h",
    "content": "#pragma once\n\n#include <string>\n\n#include \"tiles/db/tile_database.h\"\n#include \"tiles/get_tile.h\"\n\nnamespace motis {\n\nstruct tiles_data {\n  tiles_data(std::string const& path, std::size_t const db_size)\n      : db_env_{::tiles::make_tile_database(path.c_str(), db_size)},\n        db_handle_{db_env_},\n        render_ctx_{::tiles::make_render_ctx(db_handle_)},\n        pack_handle_{path.c_str()} {}\n\n  lmdb::env db_env_;\n  ::tiles::tile_db_handle db_handle_;\n  ::tiles::render_ctx render_ctx_;\n  ::tiles::pack_handle pack_handle_;\n};\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/timetable/clasz_to_mode.h",
    "content": "#pragma once\n\n#include \"nigiri/routing/clasz_mask.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace motis {\n\napi::ModeEnum to_mode(nigiri::clasz, unsigned api_version);\n\nstd::vector<api::ModeEnum> to_modes(nigiri::routing::clasz_mask_t,\n                                    unsigned api_version);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/timetable/modes_to_clasz_mask.h",
    "content": "#pragma once\n\n#include \"motis-api/motis-api.h\"\n\n#include \"nigiri/routing/clasz_mask.h\"\n\nnamespace motis {\n\nnigiri::routing::clasz_mask_t to_clasz_mask(std::vector<api::ModeEnum> const&);\n\n}"
  },
  {
    "path": "include/motis/timetable/time_conv.h",
    "content": "#pragma once\n\n#include <chrono>\n#include <cinttypes>\n\n#include \"nigiri/types.h\"\n\nnamespace motis {\n\ninline std::int64_t to_seconds(nigiri::unixtime_t const t) {\n  return std::chrono::duration_cast<std::chrono::seconds>(t.time_since_epoch())\n      .count();\n}\n\ninline std::int64_t to_seconds(nigiri::i32_minutes const t) {\n  return std::chrono::duration_cast<std::chrono::seconds>(t).count();\n}\n\ninline std::int64_t to_ms(nigiri::i32_minutes const t) {\n  return std::chrono::duration_cast<std::chrono::milliseconds>(t).count();\n}\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/transport_mode_ids.h",
    "content": "#pragma once\n\n#include \"osr/routing/profile.h\"\n\n#include \"nigiri/types.h\"\n\nnamespace motis {\n\nconstexpr auto const kOdmTransportModeId =\n    static_cast<nigiri::transport_mode_id_t>(osr::kNumProfiles);\nconstexpr auto const kRideSharingTransportModeId =\n    static_cast<nigiri::transport_mode_id_t>(osr::kNumProfiles + 1U);\nconstexpr auto const kGbfsTransportModeIdOffset =\n    static_cast<nigiri::transport_mode_id_t>(osr::kNumProfiles + 2U);\nconstexpr auto const kFlexModeIdOffset =\n    static_cast<nigiri::transport_mode_id_t>(1'000'000U);\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/tt_location_rtree.h",
    "content": "#pragma once\n\n#include \"nigiri/special_stations.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/point_rtree.h\"\n\nnamespace motis {\n\ninline point_rtree<nigiri::location_idx_t> create_location_rtree(\n    nigiri::timetable const& tt) {\n  auto t = point_rtree<nigiri::location_idx_t>{};\n  for (auto i = nigiri::location_idx_t{nigiri::kNSpecialStations};\n       i != tt.n_locations(); ++i) {\n    t.add(tt.locations_.coordinates_[i], i);\n  }\n  return t;\n}\n\n}  // namespace motis"
  },
  {
    "path": "include/motis/types.h",
    "content": "#pragma once\n\n#include <cinttypes>\n#include <memory>\n#include <optional>\n#include <vector>\n\n#include \"geo/latlng.h\"\n\n#include \"cista/reflection/comparable.h\"\n#include \"cista/strong.h\"\n\n#include \"nigiri/common/interval.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/elevators/get_state_changes.h\"\n\nnamespace nigiri {\nstruct rt_timetable;\n}\n\nnamespace motis {\n\ntemplate <typename K, typename V>\nusing vector_map = nigiri::vector_map<K, V>;\n\ntemplate <typename T>\nusing hash_set = nigiri::hash_set<T>;\n\ntemplate <typename K,\n          typename V,\n          typename Hash = cista::hash_all,\n          typename Equality = cista::equals_all>\nusing hash_map = nigiri::hash_map<K, V, Hash, Equality>;\n\ntemplate <typename T>\nusing basic_string = std::basic_string<T, cista::char_traits<T>>;\n\nusing elevator_idx_t = cista::strong<std::uint32_t, struct elevator_idx_>;\n\nusing gbfs_provider_idx_t =\n    cista::strong<std::uint16_t, struct gbfs_provider_idx_>;\n\nstruct elevator {\n  friend bool operator==(elevator const&, elevator const&) = default;\n\n  std::vector<state_change<nigiri::unixtime_t>> const& get_state_changes()\n      const {\n    return state_changes_;\n  }\n\n  std::int64_t id_;\n  std::optional<std::string> id_str_;\n  geo::latlng pos_;\n  bool status_;\n  std::string desc_;\n  std::vector<nigiri::interval<nigiri::unixtime_t>> out_of_service_;\n  std::vector<state_change<nigiri::unixtime_t>> state_changes_{\n      intervals_to_state_changes(out_of_service_, status_)};\n};\n\nusing rtt_ptr_t = std::shared_ptr<nigiri::rt_timetable>;\n\n}  // namespace motis\n"
  },
  {
    "path": "include/motis/update_rtt_td_footpaths.h",
    "content": "#pragma once\n\n#include <vector>\n\n#include \"osr/lookup.h\"\n#include \"osr/routing/route.h\"\n\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/compute_footpaths.h\"\n#include \"motis/data.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/fwd.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n\nnamespace motis {\n\nusing nodes_t = std::vector<osr::node_idx_t>;\nusing states_t = std::vector<bool>;\n\nosr::bitvec<osr::node_idx_t>& set_blocked(nodes_t const&,\n                                          states_t const&,\n                                          osr::bitvec<osr::node_idx_t>&);\n\nstd::vector<nigiri::td_footpath> get_td_footpaths(\n    osr::ways const&,\n    osr::lookup const&,\n    osr::platforms const&,\n    nigiri::timetable const&,\n    nigiri::rt_timetable const*,\n    point_rtree<nigiri::location_idx_t> const&,\n    elevators const&,\n    platform_matches_t const&,\n    nigiri::location_idx_t start_l,\n    osr::location start,\n    osr::direction,\n    osr::search_profile,\n    std::chrono::seconds max,\n    double max_matching_distance,\n    osr_parameters const&,\n    osr::bitvec<osr::node_idx_t>& blocked_mem);\n\nstd::optional<std::pair<nodes_t, states_t>> get_states_at(osr::ways const&,\n                                                          osr::lookup const&,\n                                                          elevators const&,\n                                                          nigiri::unixtime_t,\n                                                          geo::latlng const&);\n\nvoid update_rtt_td_footpaths(\n    osr::ways const&,\n    osr::lookup const&,\n    osr::platforms const&,\n    nigiri::timetable const&,\n    point_rtree<nigiri::location_idx_t> const&,\n    elevators const&,\n    platform_matches_t const&,\n    hash_set<std::pair<nigiri::location_idx_t, osr::direction>> const& tasks,\n    nigiri::rt_timetable const* old_rtt,\n    nigiri::rt_timetable&,\n    std::chrono::seconds max);\n\nvoid update_rtt_td_footpaths(osr::ways const&,\n                             osr::lookup const&,\n                             osr::platforms const&,\n                             nigiri::timetable const&,\n                             point_rtree<nigiri::location_idx_t> const&,\n                             elevators const&,\n                             elevator_footpath_map_t const&,\n                             platform_matches_t const&,\n                             nigiri::rt_timetable&,\n                             std::chrono::seconds max);\n\n}  // namespace motis\n"
  },
  {
    "path": "openapi.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  title: MOTIS API\n  description: |\n    This is the MOTIS routing API.\n\n    Overview of MOTIS API versions:\n\n    MOTIS 0.x - deprecated/discontinued\n\n    MOTIS 2.x - current, providing:\n\n    * /api/v5/{plan,trip,stoptimes,map/trips} renamed METRO mode to SUBURBAN, AREAL_LIFT to AERIAL_LIFT; since MOTIS 2.5.0\n    * /api/v4/{plan,trip,stoptimes,map/trips} new displayName property, routeShortName only contains actual route short name from source; since MOTIS 2.2.0\n    * /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 \n    * /api/v2/{plan,trip} returns Google polylines with precision=6; since MOTIS 2.0.60\n    * /api/v1/{plan,trip} returns Google polylines with precision=7 (not defined for |longitude|>107)\n    * /api/v1/* all other endpoints\n\n    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).\n  contact:\n    email: felix@triptix.tech\n  license:\n    name: MIT\n    url: https://opensource.org/license/mit\n  version: v5\nexternalDocs:\n  description: Find out more about MOTIS\n  url: https://github.com/motis-project/motis\nservers:\n  - url: https://api.transitous.org\n    description: Transitous production server\n  - url: https://staging.api.transitous.org\n    description: Transitous staging server\n  - url: http://localhost:8080\n    description: Local MOTIS server\npaths:\n  /api/v5/plan:\n    get:\n      tags:\n        - routing\n      summary: Computes optimal connections from one place to another.\n      operationId: plan\n      parameters:\n        - name: fromPlace\n          in: query\n          required: true\n          description: |\n            \\`latitude,longitude[,level]\\` tuple with\n            - latitude and longitude in degrees\n            - (optional) level: the OSM level (default: 0)\n\n            OR\n\n            stop id\n          schema:\n            type: string\n\n        - name: toPlace\n          in: query\n          required: true\n          description: |\n            \\`latitude,longitude[,level]\\` tuple with\n            - latitude and longitude in degrees\n            - (optional) level: the OSM level (default: 0)\n\n            OR\n\n            stop id\n          schema:\n            type: string\n\n        - name: radius\n          in: query\n          required: false\n          description: |\n            Experimental. Search radius in meters around the `fromPlace` / `toPlace` coordinates.\n            When set and the place is given as coordinates, all transit stops within\n            this radius are used as start/end points with zero pre-transit/post-transit time.\n            Works without OSM/street routing data loaded.\n          schema:\n            type: number\n            format: double\n\n        - name: via\n          in: query\n          required: false\n          description: |\n            List of via stops to visit (only stop IDs, no coordinates allowed for now).\n            Also see the optional parameter `viaMinimumStay` to set a set a minimum stay duration for each via stop.\n          schema:\n            type: array\n            maxItems: 2\n            items:\n              type: string\n          explode: false\n\n        - name: viaMinimumStay\n          in: query\n          required: false\n          description: |\n            Optional. If not set, the default is `0,0` - no stay required.\n            \n            For each `via` stop a minimum stay duration in minutes.\n            \n            The value `0` signals that it's allowed to stay in the same trip.\n            This enables via stays without counting a transfer and can lead \n            to better connections with less transfers. Transfer connections can\n            still be found with `viaMinimumStay=0`.\n          schema:\n            default: [ 0, 0 ]\n            type: array\n            maxItems: 2\n            items:\n              type: integer\n          explode: false\n\n        - name: time\n          in: query\n          required: false\n          description: |\n            Optional. Defaults to the current time.\n            \n            Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n          schema:\n            type: string\n            format: date-time\n\n        - name: maxTransfers\n          in: query\n          required: false\n          description: |\n            The maximum number of allowed transfers (i.e. interchanges between transit legs,\n            pre- and postTransit do not count as transfers).\n            `maxTransfers=0` searches for direct transit connections without any transfers.\n            If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n            send an empty `transitModes` parameter instead.\n\n            If not provided, the routing uses the server-side default value\n            which is hardcoded and very high to cover all use cases.\n            \n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the fastest) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n\n            In plan endpoints before v3, the behavior is off by one,\n            i.e. `maxTransfers=0` only returns non-transit connections.\n          schema:\n            type: integer\n\n        - name: maxTravelTime\n          in: query\n          required: false\n          description: |\n            The maximum travel time in minutes.\n            If not provided, the routing to uses the value\n            hardcoded in the server which is usually quite high.\n            \n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the least transfers) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n          schema:\n            type: integer\n\n        - name: minTransferTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 0 minutes.\n            \n            Minimum transfer time for each transfer in minutes.\n          schema:\n            type: integer\n            default: 0\n\n        - name: additionalTransferTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 0 minutes.\n            \n            Additional transfer time reserved for each transfer in minutes.\n          schema:\n            type: integer\n            default: 0\n\n        - name: transferTimeFactor\n          in: query\n          required: false\n          description: |\n            Optional. Default is 1.0\n            \n            Factor to multiply minimum required transfer times with.\n            Values smaller than 1.0 are not supported.\n          schema:\n            type: number\n            default: 1.0\n\n        - name: maxMatchingDistance\n          in: query\n          required: false\n          description: |\n            Optional. Default is 25 meters.\n            \n            Maximum matching distance in meters to match geo coordinates to the street network.\n          schema:\n            type: number\n            default: 25\n\n        - name: pedestrianProfile\n          in: query\n          required: false\n          description: |\n            Optional. Default is `FOOT`.\n            \n            Accessibility profile to use for pedestrian routing in transfers\n            between transit connections, on the first mile, and last mile.\n          schema:\n            $ref: '#/components/schemas/PedestrianProfile'\n            default: FOOT\n\n        - name: pedestrianSpeed\n          in: query\n          required: false\n          description: |\n            Optional\n\n            Average speed for pedestrian routing.\n          schema:\n            $ref: '#/components/schemas/PedestrianSpeed'\n\n        - name: cyclingSpeed\n          in: query\n          required: false\n          description: |\n            Optional\n\n            Average speed for bike routing.\n          schema:\n            $ref: '#/components/schemas/CyclingSpeed'\n\n        - name: elevationCosts\n          in: query\n          required: false\n          description: |\n            Optional. Default is `NONE`.\n\n            Set an elevation cost profile, to penalize routes with incline.\n            - `NONE`: No additional costs for elevations. This is the default behavior\n            - `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.\n            - `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.\n\n            As using an elevation costs profile will increase the travel duration,\n            routing through steep terrain may exceed the maximal allowed duration,\n            causing a location to appear unreachable.\n            Increasing the maximum travel time for these segments may resolve this issue.\n\n            The profile is used for direct routing, on the first mile, and last mile.\n\n            Elevation cost profiles are currently used by following street modes:\n            - `BIKE`\n          schema:\n            $ref: '#/components/schemas/ElevationCosts'\n            default: NONE\n\n        - name: useRoutedTransfers\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n            \n            Whether to use transfers routed on OpenStreetMap data.\n          schema:\n            type: boolean\n            default: false\n\n        - name: detailedTransfers\n          in: query\n          required: false\n          description: |\n            Controls if transfer polylines and step instructions are returned.\n\n            If not set, this parameter inherits the value of `detailedLegs`.\n\n            - true: Compute transfer polylines and step instructions.\n            - false: Return empty `legGeometry` and omit `steps` for transfers.\n          schema:\n            type: boolean\n\n        - name: detailedLegs\n          in: query\n          required: false\n          description: |\n            Controls if `legGeometry` and `steps` are returned for direct legs,\n            pre-/post-transit legs and transit legs.\n          schema:\n            type: boolean\n            default: true\n\n        - name: joinInterlinedLegs\n          in: query\n          description: |\n            Optional. Default is `true`.\n            \n            Controls if a journey section with stay-seated transfers is returned:\n            - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.).\n              Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`.\n            - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops\n          schema:\n            type: boolean\n            default: true\n\n        - name: transitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n            Allowed modes for the transit part. If empty, no transit connections will be computed.\n            For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n          schema:\n            default:\n              - TRANSIT\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: directModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `WALK` which will compute walking routes as direct connections.\n            \n            Modes used for direction connections from start to destination without using transit.\n            Results will be returned on the `direct` key.\n            \n            Note: Direct connections will only be returned on the first call. For paging calls, they can be omitted.\n            \n            Note: Transit connections that are slower than the fastest direct connection will not show up.\n            This is being used as a cut-off during transit routing to speed up the search.\n            To prevent this, it's possible to send two separate requests (one with only `transitModes` and one with only `directModes`).\n\n            Note: the output `direct` array will stay empty if the input param `maxDirectTime` makes any direct trip impossible.\n            \n            Only non-transit modes such as `WALK`, `BIKE`, `CAR`, `BIKE_SHARING`, etc. can be used.\n          schema:\n            default:\n              - WALK\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: preTransitModes\n          in: query\n          required: false\n          description: |\n            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`).\n            \n            A list of modes that are allowed to be used from the `from` coordinate to the first transit stop. Example: `WALK,BIKE_SHARING`.\n          schema:\n            default:\n              - WALK\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: postTransitModes\n          in: query\n          required: false\n          description: |\n            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`).\n            \n            A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.\n          schema:\n            default:\n              - WALK\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: directRentalFormFactors\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies to direct connections.\n            \n            A list of vehicle type form factors that are allowed to be used for direct connections.\n            If empty (the default), all form factors are allowed.\n            Example: `BICYCLE,SCOOTER_STANDING`.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/RentalFormFactor'\n          explode: false\n\n        - name: preTransitRentalFormFactors\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`).\n            \n            A list of vehicle type form factors that are allowed to be used from the `from` coordinate to the first transit stop.\n            If empty (the default), all form factors are allowed.\n            Example: `BICYCLE,SCOOTER_STANDING`.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/RentalFormFactor'\n          explode: false\n\n        - name: postTransitRentalFormFactors\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`).\n\n            A list of vehicle type form factors that are allowed to be used from the last transit stop to the `to` coordinate.\n            If empty (the default), all form factors are allowed.\n            Example: `BICYCLE,SCOOTER_STANDING`.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/RentalFormFactor'\n          explode: false\n\n        - name: directRentalPropulsionTypes\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies to direct connections.\n            \n            A list of vehicle type form factors that are allowed to be used for direct connections.\n            If empty (the default), all propulsion types are allowed.\n            Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/RentalPropulsionType'\n          explode: false\n\n        - name: preTransitRentalPropulsionTypes\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`).\n            \n            A list of vehicle propulsion types that are allowed to be used from the `from` coordinate to the first transit stop.\n            If empty (the default), all propulsion types are allowed.\n            Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/RentalPropulsionType'\n          explode: false\n\n        - name: postTransitRentalPropulsionTypes\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`).\n            \n            A list of vehicle propulsion types that are allowed to be used from the last transit stop to the `to` coordinate.\n            If empty (the default), all propulsion types are allowed.\n            Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/RentalPropulsionType'\n          explode: false\n\n        - name: directRentalProviders\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies to direct connections.\n            \n            A list of rental providers that are allowed to be used for direct connections.\n            If empty (the default), all providers are allowed.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: directRentalProviderGroups\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies to direct connections.\n            \n            A list of rental provider groups that are allowed to be used for direct connections.\n            If empty (the default), all providers are allowed.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: preTransitRentalProviders\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`).\n            \n            A list of rental providers that are allowed to be used from the `from` coordinate to the first transit stop.\n            If empty (the default), all providers are allowed.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: preTransitRentalProviderGroups\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`).\n            \n            A list of rental provider groups that are allowed to be used from the `from` coordinate to the first transit stop.\n            If empty (the default), all providers are allowed.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: postTransitRentalProviders\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`).\n            \n            A list of rental providers that are allowed to be used from the last transit stop to the `to` coordinate.\n            If empty (the default), all providers are allowed.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: postTransitRentalProviderGroups\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`).\n            \n            A list of rental provider groups that are allowed to be used from the last transit stop to the `to` coordinate.\n            If empty (the default), all providers are allowed.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: ignoreDirectRentalReturnConstraints\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Default is `false`.\n            \n            If set to `true`, the routing will ignore rental return constraints for direct connections,\n            allowing the rental vehicle to be parked anywhere.\n          schema:\n            type: boolean\n            default: false\n\n        - name: ignorePreTransitRentalReturnConstraints\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Default is `false`.\n            \n            If set to `true`, the routing will ignore rental return constraints for the part from the `from` coordinate to the first transit stop,\n            allowing the rental vehicle to be parked anywhere.\n          schema:\n            type: boolean\n            default: false\n\n        - name: ignorePostTransitRentalReturnConstraints\n          in: query\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n            \n            Optional. Default is `false`.\n            \n            If set to `true`, the routing will ignore rental return constraints for the part from the last transit stop to the `to` coordinate,\n            allowing the rental vehicle to be parked anywhere.\n          schema:\n            type: boolean\n            default: false\n\n        - name: numItineraries\n          in: query\n          required: false\n          description: |\n            The minimum number of itineraries to compute.\n            This is only relevant if `timetableView=true`.\n            The default value is 5.\n          schema:\n            type: integer\n            default: 5\n\n        - name: maxItineraries\n          in: query\n          required: false\n          description: |\n            Optional. By default all computed itineraries will be returned\n            \n            The maximum number of itineraries to compute.\n            This is only relevant if `timetableView=true`.\n            \n            Note: With the current implementation, setting this to a lower\n            number will not result in any speedup.\n            \n            Note: The number of returned itineraries might be slightly higher\n            than `maxItineraries` as there might be several itineraries with\n            the same departure time but different number of transfers. In order\n            to not miss any itineraries for paging, either none or all\n            itineraries with the same departure time have to be returned.\n          schema:\n            type: integer\n\n        - name: pageCursor\n          in: query\n          required: false\n          description: |\n            Use the cursor to go to the next \"page\" of itineraries.\n            Copy the cursor from the last response and keep the original request as is.\n            This will enable you to search for itineraries in the next or previous time-window.\n          schema:\n            type: string\n\n        - name: timetableView\n          in: query\n          required: false\n          description: |\n            Optional. Default is `true`.\n            \n            Search for the best trip options within a time window.\n            If true two itineraries are considered optimal\n            if one is better on arrival time (earliest wins)\n            and the other is better on departure time (latest wins).\n            In combination with arriveBy this parameter cover the following use cases:\n            \n            `timetable=false` = waiting for the first transit departure/arrival is considered travel time:\n              - `arriveBy=true`: event (e.g. a meeting) starts at 10:00 am,\n                compute the best journeys that arrive by that time (maximizes departure time)\n              - `arriveBy=false`: event (e.g. a meeting) ends at 11:00 am,\n                compute the best journeys that depart after that time\n            \n            `timetable=true` = optimize \"later departure\" + \"earlier arrival\" and give all options over a time window:\n              - `arriveBy=true`: the time window around `date` and `time` refers to the arrival time window\n              - `arriveBy=false`: the time window around `date` and `time` refers to the departure time window\n          schema:\n            type: boolean\n            default: true\n\n        - name: arriveBy\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: |\n            Optional. Default is `false`.\n            \n              - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time\n              - `arriveBy=false`: the parameters `date` and `time` refer to the departure time\n\n        - name: searchWindow\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15 minutes which is `900`.\n            \n            The length of the search-window in seconds. Default value 15 minutes.\n            \n              - `arriveBy=true`: number of seconds between the earliest departure time and latest departure time\n              - `arriveBy=false`: number of seconds between the earliest arrival time and the latest arrival time\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n        - name: requireBikeTransport\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: |\n            Optional. Default is `false`.\n            \n            If set to `true`, all used transit trips are required to allow bike carriage.\n\n        - name: requireCarTransport\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: |\n            Optional. Default is `false`.\n            \n            If set to `true`, all used transit trips are required to allow car carriage.\n\n        - name: maxPreTransitTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15min which is `900`.\n            Maximum time in seconds for the first street leg.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n        - name: maxPostTransitTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15min which is `900`.\n            Maximum time in seconds for the last street leg.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n        - name: maxDirectTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 30min which is `1800`.\n            Maximum time in seconds for direct connections.\n            Is limited by server config variable `street_routing_max_direct_seconds`.\n          schema:\n            type: integer\n            default: 1800\n            minimum: 0\n\n        - name: fastestDirectFactor\n          in: query\n          required: false\n          description: |\n            Optional. Experimental. Default is `1.0`.\n            Factor with which the duration of the fastest direct non-public-transit connection is multiplied.\n            Values > 1.0 allow transit connections that are slower than the fastest direct non-public-transit connection to be found.\n          schema:\n            type: number\n            default: 1.0\n            minimum: 0\n\n        - name: timeout\n          in: query\n          required: false\n          description: Optional. Query timeout in seconds.\n          schema:\n            type: integer\n            minimum: 0\n\n        - name: passengers\n          in: query\n          required: false\n          description: Optional. Experimental. Number of passengers (e.g. for ODM or price calculation)\n          schema:\n            type: integer\n            minimum: 1\n\n        - name: luggage\n          in: query\n          required: false\n          description: |\n            Optional. Experimental. Number of luggage pieces; base unit: airline cabin luggage (e.g. for ODM or price calculation)\n          schema:\n            type: integer\n            minimum: 1\n\n        - name: slowDirect\n          in: query\n          required: false\n          description: Optional. Experimental. Adds overtaken direct public transit connections.\n          schema:\n            type: boolean\n            default: false\n\n        - name: fastestSlowDirectFactor\n          in: query\n          required: false\n          description: |\n            Optional. \n            Factor with which the duration of the fastest slowDirect connection is multiplied.\n            Values > 1.0 allow connections that are slower than the fastest direct transit connection to be found.\n            Values < 1.0 will return all slowDirect connections.\n          schema:\n            type: number\n            default: 3.0\n            minimum: 0\n\n        - name: withFares\n          in: query\n          required: false\n          description: Optional. Experimental. If set to true, the response will contain fare information.\n          schema:\n            type: boolean\n            default: false\n\n        - name: withScheduledSkippedStops\n          in: query\n          required: false\n          description: Optional. Include intermediate stops where passengers can not alight/board according to schedule.\n          schema:\n            type: boolean\n            default: false\n\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: algorithm\n          in: query\n          required: false\n          description: algorithm to use\n          schema:\n            type: string\n            enum:\n              - RAPTOR\n              - PONG\n              - TB\n            default: PONG\n      responses:\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: routing result\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - requestParameters\n                  - debugOutput\n                  - from\n                  - to\n                  - direct\n                  - itineraries\n                  - previousPageCursor\n                  - nextPageCursor\n                properties:\n                  requestParameters:\n                    description: \"the routing query\"\n                    type: object\n                    additionalProperties:\n                      type: string\n                  debugOutput:\n                    description: \"debug statistics\"\n                    type: object\n                    additionalProperties:\n                      type: integer\n                  from:\n                    $ref: '#/components/schemas/Place'\n                  to:\n                    $ref: '#/components/schemas/Place'\n                  direct:\n                    description: |\n                      Direct trips by `WALK`, `BIKE`, `CAR`, etc. without time-dependency.\n                      The starting time (`arriveBy=false`) / arrival time (`arriveBy=true`) is always the queried `time` parameter (set to \\\"now\\\" if not set).\n                      But all `direct` connections are meant to be independent of absolute times.\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/Itinerary'\n                  itineraries:\n                    description: list of itineraries\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/Itinerary'\n                  previousPageCursor:\n                    description: |\n                      Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page.\n                      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.\n                    type: string\n                  nextPageCursor:\n                    description: |\n                      Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page.\n                      The next page is a set of itineraries departing AFTER the last itinerary in this result.\n                    type: string\n\n  /api/v1/one-to-many:\n    get:\n      tags:\n        - routing\n      summary: |\n        Street routing from one to many places or many to one.\n        The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the query.\n      operationId: oneToMany\n      parameters:\n        - name: one\n          in: query\n          required: true\n          description: geo location as latitude;longitude\n          schema:\n            type: string\n        - name: many\n          in: query\n          required: true\n          description: |\n            geo locations as latitude;longitude,latitude;longitude,...\n\n            The number of accepted locations is limited by server config variable `onetomany_max_many`.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n        - name: mode\n          in: query\n          required: true\n          description: |\n            routing profile to use (currently supported: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`)\n          schema:\n            $ref: '#/components/schemas/Mode'\n        - name: max\n          in: query\n          required: true\n          description: maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`.\n          schema:\n            type: number\n        - name: maxMatchingDistance\n          in: query\n          required: true\n          description: maximum matching distance in meters to match geo coordinates to the street network\n          schema:\n            type: number\n        - name: elevationCosts\n          in: query\n          required: false\n          description: |\n            Optional. Default is `NONE`.\n\n            Set an elevation cost profile, to penalize routes with incline.\n            - `NONE`: No additional costs for elevations. This is the default behavior\n            - `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.\n            - `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.\n\n            As using an elevation costs profile will increase the travel duration,\n            routing through steep terrain may exceed the maximal allowed duration,\n            causing a location to appear unreachable.\n            Increasing the maximum travel time for these segments may resolve this issue.\n\n            Elevation cost profiles are currently used by following street modes:\n            - `BIKE`\n          schema:\n            $ref: '#/components/schemas/ElevationCosts'\n            default: NONE\n        - name: arriveBy\n          in: query\n          required: true\n          description: |\n            true = many to one\n            false = one to many\n          schema:\n            type: boolean\n        - name: withDistance\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n            If true, the response includes the distance in meters\n            for each path. This requires path reconstruction and\n            is slower than duration-only queries.\n          schema:\n            type: boolean\n            default: false\n      responses:\n        '200':\n          description: |\n            A list of durations.\n            If no path was found, the object is empty.\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Duration'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n    post:\n      tags:\n        - routing\n      summary: |\n        Street routing from one to many places or many to one.\n        The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the request body.\n      operationId: oneToManyPost\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/OneToManyParams'\n      responses:\n        '200':\n          description: |\n            A list of durations.\n            If no path was found, the object is empty.\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Duration'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /api/experimental/one-to-many-intermodal:\n    get:\n      tags:\n        - routing\n      summary: |\n        One to many routing\n        Computes the minimal duration from one place to many or vice versa.\n        The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the query.\n      operationId: oneToManyIntermodal\n      parameters:\n        - name: one\n          in: query\n          required: true\n          description: geo location as latitude;longitude\n          schema:\n            type: string\n\n        - name: many\n          in: query\n          required: true\n          description: |\n            geo locations as latitude;longitude,latitude;longitude,...\n\n            The number of accepted locations is limited by server config variable `onetomany_max_many`.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: time\n          in: query\n          required: false\n          description: |\n            Optional. Defaults to the current time.\n\n            Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n          schema:\n            type: string\n            format: date-time\n\n        - name: maxTravelTime\n          in: query\n          required: false\n          description: |\n            The maximum travel time in minutes.\n            If not provided, the routing uses the value\n            hardcoded in the server which is usually quite high.\n\n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the least transfers) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n          schema:\n            type: integer\n\n        - name: maxMatchingDistance\n          in: query\n          required: false\n          description: maximum matching distance in meters to match geo coordinates to the street network\n          schema:\n            type: number\n            default: 25\n\n        - name: arriveBy\n          in: query\n          required: false\n          description: |\n            Optional. Defaults to false, i.e. one to many search\n\n            true = many to one\n            false = one to many\n          schema:\n            type: boolean\n            default: false\n\n        - name: maxTransfers\n          in: query\n          required: false\n          description: |\n            The maximum number of allowed transfers (i.e. interchanges between transit legs,\n            pre- and postTransit do not count as transfers).\n            `maxTransfers=0` searches for direct transit connections without any transfers.\n            If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n            send an empty `transitModes` parameter instead.\n\n            If not provided, the routing uses the server-side default value\n            which is hardcoded and very high to cover all use cases.\n\n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the fastest) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n          schema:\n            type: integer\n\n        - name: minTransferTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 0 minutes.\n\n            Minimum transfer time for each transfer in minutes.\n          schema:\n            type: integer\n            default: 0\n\n        - name: additionalTransferTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 0 minutes.\n\n            Additional transfer time reserved for each transfer in minutes.\n          schema:\n            type: integer\n            default: 0\n\n        - name: transferTimeFactor\n          in: query\n          required: false\n          description: |\n            Optional. Default is 1.0\n\n            Factor to multiply minimum required transfer times with.\n            Values smaller than 1.0 are not supported.\n          schema:\n            type: number\n            default: 1.0\n\n        - name: useRoutedTransfers\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n\n            Whether to use transfers routed on OpenStreetMap data.\n          schema:\n            type: boolean\n            default: false\n\n        - name: pedestrianProfile\n          in: query\n          required: false\n          description: |\n            Optional. Default is `FOOT`.\n\n            Accessibility profile to use for pedestrian routing in transfers\n            between transit connections and the first and last mile respectively.\n          schema:\n            $ref: \"#/components/schemas/PedestrianProfile\"\n            default: FOOT\n\n        - name: pedestrianSpeed\n          in: query\n          required: false\n          description: |\n            Optional\n\n            Average speed for pedestrian routing.\n          schema:\n            $ref: \"#/components/schemas/PedestrianSpeed\"\n\n        - name: cyclingSpeed\n          in: query\n          required: false\n          description: |\n            Optional\n\n            Average speed for bike routing.\n          schema:\n            $ref: \"#/components/schemas/CyclingSpeed\"\n\n        - name: elevationCosts\n          in: query\n          required: false\n          description: |\n            Optional. Default is `NONE`.\n\n            Set an elevation cost profile, to penalize routes with incline.\n            - `NONE`: No additional costs for elevations. This is the default behavior\n            - `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.\n            - `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.\n\n            As using an elevation costs profile will increase the travel duration,\n            routing through steep terrain may exceed the maximal allowed duration,\n            causing a location to appear unreachable.\n            Increasing the maximum travel time for these segments may resolve this issue.\n\n            The profile is used for routing on both the first and last mile.\n\n            Elevation cost profiles are currently used by following street modes:\n            - `BIKE`\n          schema:\n            $ref: \"#/components/schemas/ElevationCosts\"\n            default: NONE\n\n        - name: transitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n            Allowed modes for the transit part. If empty, no transit connections will be computed.\n            For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n          schema:\n            type: array\n            items:\n              $ref: \"#/components/schemas/Mode\"\n            default:\n              - TRANSIT\n          explode: false\n\n        - name: preTransitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n\n            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`.\n          schema:\n            type: array\n            items:\n              $ref: \"#/components/schemas/Mode\"\n            default:\n              - WALK\n          explode: false\n\n        - name: postTransitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n\n            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`.\n          schema:\n            type: array\n            items:\n              $ref: \"#/components/schemas/Mode\"\n            default:\n              - WALK\n          explode: false\n\n        - name: directMode\n          in: query\n          required: false\n          description: |\n            Default is `WALK` which will compute walking routes as direct connections.\n\n            Mode used for direction connections from start to destination without using transit.\n\n            Currently supported non-transit modes: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`\n          schema:\n            $ref: \"#/components/schemas/Mode\"\n            default: WALK\n\n        - name: maxPreTransitTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15min which is `900`.\n            Maximum time in seconds for the first street leg.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n        - name: maxPostTransitTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15min which is `900`.\n            Maximum time in seconds for the last street leg.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n        - name: maxDirectTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 30min which is `1800`.\n            Maximum time in seconds for direct connections.\n\n            If a value smaller than either `maxPreTransitTime` or\n            `maxPostTransitTime` is used, their maximum is set instead.\n            Is limited by server config variable `street_routing_max_direct_seconds`.\n          schema:\n            type: integer\n            default: 1800\n            minimum: 0\n\n        - name: withDistance\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n            If true, the response includes the distance in meters\n            for each path. This requires path reconstruction and\n            is slower than duration-only queries.\n\n            `withDistance` is currently limited to street routing.\n          schema:\n            type: boolean\n            default: false\n\n        - name: requireBikeTransport\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n\n            If set to `true`, all used transit trips are required to allow bike carriage.\n          schema:\n            type: boolean\n            default: false\n\n        - name: requireCarTransport\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n\n            If set to `true`, all used transit trips are required to allow car carriage.\n          schema:\n            type: boolean\n            default: false\n\n      responses:\n        \"200\":\n          description: |\n            A list of durations.\n            If no path was found, the object is empty.\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/OneToManyIntermodalResponse\"\n        \"400\":\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"422\":\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"500\":\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n    post:\n      tags:\n        - routing\n      summary: |\n        One to many routing\n        Computes the minimal duration from one place to many or vice versa.\n        The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the request body.\n      operationId: oneToManyIntermodalPost\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: \"#/components/schemas/OneToManyIntermodalParams\"\n      responses:\n        \"200\":\n          description: |\n            A list of durations.\n            If no path was found, the object is empty.\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/OneToManyIntermodalResponse\"\n        \"400\":\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"422\":\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"500\":\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n\n  /api/v1/one-to-all:\n    get:\n      tags:\n        - routing\n      summary: |\n        Computes all reachable locations from a given stop within a set duration.\n        Each result entry will contain the fastest travel duration and the number of connections used.\n      operationId: oneToAll\n      parameters:\n        - name: one\n          in: query\n          required: true\n          description: |\n            \\`latitude,longitude[,level]\\` tuple with\n            - latitude and longitude in degrees\n            - (optional) level: the OSM level (default: 0)\n\n            OR\n\n            stop id\n          schema:\n            type: string\n        - name: time\n          in: query\n          required: false\n          description: |\n            Optional. Defaults to the current time.\n\n            Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n          schema:\n            type: string\n            format: date-time\n        - name: maxTravelTime\n          in: query\n          required: true\n          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.\n          schema:\n            type: integer\n        - name: arriveBy\n          in: query\n          required: false\n          description: |\n            true = all to one,\n            false = one to all\n          schema:\n            type: boolean\n            default: false\n        - name: maxTransfers\n          in: query\n          required: false\n          description: |\n            The maximum number of allowed transfers (i.e. interchanges between transit legs,\n            pre- and postTransit do not count as transfers).\n            `maxTransfers=0` searches for direct transit connections without any transfers.\n            If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n            send an empty `transitModes` parameter instead.\n\n            If not provided, the routing uses the server-side default value\n            which is hardcoded and very high to cover all use cases.\n            \n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the fastest) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n\n            In plan endpoints before v3, the behavior is off by one,\n            i.e. `maxTransfers=0` only returns non-transit connections.\n          schema:\n            type: integer\n        - name: minTransferTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 0 minutes.\n\n            Minimum transfer time for each transfer in minutes.\n          schema:\n            type: integer\n            default: 0\n\n        - name: additionalTransferTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 0 minutes.\n\n            Additional transfer time reserved for each transfer in minutes.\n          schema:\n            type: integer\n            default: 0\n\n        - name: transferTimeFactor\n          in: query\n          required: false\n          description: |\n            Optional. Default is 1.0\n\n            Factor to multiply minimum required transfer times with.\n            Values smaller than 1.0 are not supported.\n          schema:\n            type: number\n            default: 1.0\n\n        - name: maxMatchingDistance\n          in: query\n          required: false\n          description: |\n            Optional. Default is 25 meters.\n\n            Maximum matching distance in meters to match geo coordinates to the street network.\n          schema:\n            type: number\n            default: 25\n\n        - name: useRoutedTransfers\n          in: query\n          required: false\n          description: |\n            Optional. Default is `false`.\n\n            Whether to use transfers routed on OpenStreetMap data.\n          schema:\n            type: boolean\n            default: false\n        - name: pedestrianProfile\n          in: query\n          required: false\n          description: |\n            Optional. Default is `FOOT`.\n\n            Accessibility profile to use for pedestrian routing in transfers\n            between transit connections and the first and last mile respectively.\n          schema:\n            $ref: '#/components/schemas/PedestrianProfile'\n            default: FOOT\n\n        - name: pedestrianSpeed\n          in: query\n          required: false\n          description: |\n            Optional\n\n            Average speed for pedestrian routing.\n          schema:\n            $ref: '#/components/schemas/PedestrianSpeed'\n\n        - name: cyclingSpeed\n          in: query\n          required: false\n          description: |\n            Optional\n\n            Average speed for bike routing.\n          schema:\n            $ref: '#/components/schemas/CyclingSpeed'\n\n        - name: elevationCosts\n          in: query\n          required: false\n          description: |\n            Optional. Default is `NONE`.\n\n            Set an elevation cost profile, to penalize routes with incline.\n            - `NONE`: No additional costs for elevations. This is the default behavior\n            - `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.\n            - `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.\n\n            As using an elevation costs profile will increase the travel duration,\n            routing through steep terrain may exceed the maximal allowed duration,\n            causing a location to appear unreachable.\n            Increasing the maximum travel time for these segments may resolve this issue.\n\n            The profile is used for routing on both the first and last mile.\n\n            Elevation cost profiles are currently used by following street modes:\n            - `BIKE`\n          schema:\n            $ref: '#/components/schemas/ElevationCosts'\n            default: NONE\n\n        - name: transitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n            Allowed modes for the transit part. If empty, no transit connections will be computed.\n            For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n          schema:\n            default:\n              - TRANSIT\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: preTransitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set:\n              - `arriveBy=true`: Currently not used\n              - `arriveBy=false`: Only applies if the `one` place is a coordinate (not a transit stop).\n\n            A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.\n          schema:\n            default:\n              - WALK\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: postTransitModes\n          in: query\n          required: false\n          description: |\n            Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set:\n              - `arriveBy=true`: Only applies if the `one` place is a coordinate (not a transit stop).\n              - `arriveBy=false`: Currently not used\n\n            A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.\n          schema:\n            default:\n              - WALK\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: requireBikeTransport\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: |\n            Optional. Default is `false`.\n\n            If set to `true`, all used transit trips are required to allow bike carriage.\n\n        - name: requireCarTransport\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: |\n            Optional. Default is `false`.\n\n            If set to `true`, all used transit trips are required to allow car carriage.\n\n        - name: maxPreTransitTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15min which is `900`.\n              - `arriveBy=true`: Currently not used\n              - `arriveBy=false`: Maximum time in seconds for the street leg at `one` location.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n        - name: maxPostTransitTime\n          in: query\n          required: false\n          description: |\n            Optional. Default is 15min which is `900`.\n              - `arriveBy=true`: Maximum time in seconds for the street leg at `one` location.\n              - `arriveBy=false`: Currently not used\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          schema:\n            type: integer\n            default: 900\n            minimum: 0\n\n      responses:\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: |\n            The starting position and a list of all reachable stops\n            If no paths are found, the reachable list is empty.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Reachable'\n\n  /api/v1/reverse-geocode:\n    get:\n      tags:\n        - geocode\n      summary: Translate coordinates to the closest address(es)/places/stops.\n      operationId: reverseGeocode\n      parameters:\n        - name: place\n          in: query\n          required: true\n          description: latitude, longitude in degrees\n          schema:\n            type: string\n        - name: type\n          in: query\n          required: false\n          description: |\n            Optional. Default is all types.\n            \n            Only return results of the given type.\n            For example, this can be used to allow only `ADDRESS` and `STOP` results.\n          schema:\n            $ref: '#/components/schemas/LocationType'\n        - name: numResults\n          in: query\n          required: false\n          description: |\n            Optional. Number of results to return.\n\n            If omitted, 5 results are returned by default.\n            Must be <= server config variable `reverse_geocode_max_results`.\n          schema:\n            type: integer\n            minimum: 1\n      responses:\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: A list of guesses to resolve the coordinates to a location\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Match'\n\n  /api/v1/geocode:\n    get:\n      tags:\n        - geocode\n      summary: Autocompletion & geocoding that resolves user input addresses including coordinates\n      operationId: geocode\n      parameters:\n        - name: text\n          in: query\n          required: true\n          description: the (potentially partially typed) address to resolve\n          schema:\n            type: string\n\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap\n            (usually ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: type\n          in: query\n          required: false\n          description: |\n            Optional. Default is all types.\n            \n            Only return results of the given types.\n            For example, this can be used to allow only `ADDRESS` and `STOP` results.\n          schema:\n            $ref: '#/components/schemas/LocationType'\n\n        - name: mode\n          in: query\n          required: false\n          description: |\n            Optional. Filter stops by available transport modes.\n            Defaults to applying no filter.\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n          explode: false\n\n        - name: place\n          in: query\n          required: false\n          description: |\n            Optional. Used for biasing results towards the coordinate.\n            \n            Format: latitude,longitude in degrees\n          schema:\n            type: string\n\n        - name: placeBias\n          in: query\n          required: false\n          description: |\n            Optional. Used for biasing results towards the coordinate. Higher number = higher bias.\n          schema:\n            type: number\n            default: 1\n        - name: numResults\n          in: query\n          required: false\n          description: |\n            Optional. Number of suggestions to return.\n            \n            If omitted, 10 suggestions are returned by default.\n            Must be <= server config variable `geocode_max_suggestions`.\n          schema:\n            type: integer\n            minimum: 1\n\n      responses:\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: A list of guesses to resolve the text to a location\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Match'\n\n  /api/v5/trip:\n    get:\n      tags:\n        - timetable\n      summary: Get a trip as itinerary\n      operationId: trip\n      parameters:\n        - name: tripId\n          in: query\n          schema:\n            type: string\n          required: true\n          description: trip identifier (e.g. from an itinerary leg or stop event)\n        - name: withScheduledSkippedStops\n          in: query\n          required: false\n          description: Optional. Include intermediate stops where passengers can not alight/board according to schedule.\n          schema:\n            type: boolean\n            default: false\n        - name: detailedLegs\n          in: query\n          required: false\n          description: |\n            Controls if `legGeometry` is returned for transit legs.\n\n            The default value is `true`.\n          schema:\n            type: boolean\n            default: true\n        - name: joinInterlinedLegs\n          in: query\n          description: |\n            Optional. Default is `true`.\n            \n            Controls if a trip with stay-seated transfers is returned:\n            - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.).\n              Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`.\n            - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops\n          schema:\n            type: boolean\n            default: true\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n      responses:\n        '200':\n          description: the requested trip as itinerary\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Itinerary'\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /api/v5/stoptimes:\n    get:\n      tags:\n        - timetable\n      summary: Get the next N departures or arrivals of a stop sorted by time\n      operationId: stoptimes\n      parameters:\n        - name: stopId\n          in: query\n          schema:\n            type: string\n          required: false\n          description: stop id of the stop to retrieve departures/arrivals for\n\n        - name: center\n          in: query\n          schema:\n            type: string\n          required: false\n          description: |\n            Anchor coordinate. Format: latitude,longitude pair.\n            Used as fallback when \"stopId\" is missing or can't be found.\n            If both are provided and \"stopId\" resolves, \"stopId\" is used.\n            If \"stopId\" does not resolve, \"center\" is used instead. \"radius\" is\n            required when querying by \"center\" (i.e. without a valid \"stopId\").\n\n        - name: time\n          in: query\n          required: false\n          description: |\n            Optional. Defaults to the current time.\n          schema:\n            type: string\n            format: date-time\n\n        - name: arriveBy\n          in: query\n          required: false\n          schema:\n            type: boolean\n            default: false\n          description: |\n            Optional. Default is `false`.\n            \n              - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time\n              - `arriveBy=false`: the parameters `date` and `time` refer to the departure time\n\n        - name: direction\n          in: query\n          required: false\n          schema:\n            type: string\n            enum:\n              - EARLIER\n              - LATER\n          description: |\n            This parameter will be ignored in case `pageCursor` is set.\n            \n            Optional. Default is\n              - `LATER` for `arriveBy=false`\n              - `EARLIER` for `arriveBy=true`\n            \n            The response will contain the next `n` arrivals / departures\n            in case `EARLIER` is selected and the previous `n`\n            arrivals / departures if `LATER` is selected.\n\n        - name: window\n          in: query\n          required: false\n          description: |\n            Optional. Window in seconds around `time`.\n            Limiting the response to those that are at most `window` seconds aways in time.\n            If both `n` and `window` are set, it uses whichever returns more.\n          schema:\n            type: integer\n            minimum: 0\n\n        - name: mode\n          in: query\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/Mode'\n            default:\n              - TRANSIT\n          explode: false\n          description: |\n            Optional. Default is all transit modes.\n            \n            Only return arrivals/departures of the given modes.\n\n        - name: n\n          in: query\n          schema:\n            type: integer\n          required: false\n          description: |\n            Minimum number of events to return. If both `n` and `window`\n            are provided, the API uses whichever returns more events.\n\n        - name: radius\n          in: query\n          schema:\n            type: integer\n          required: false\n          description: |\n            Optional. Radius in meters.\n            \n            Default is that only stop times of the parent of the stop itself\n            and all stops with the same name (+ their child stops) are returned.\n            \n            If set, all stops at parent stations and their child stops in the specified radius\n            are returned.\n\n        - name: exactRadius\n          in: query\n          schema:\n            type: boolean\n            default: false\n          required: false\n          description: |\n            Optional. Default is `false`.\n            \n            If set to `true`, only stations that are phyiscally in the radius are considered.\n            If set to `false`, additionally to the stations in the radius, equivalences with the same name and children are considered.\n\n        - name: fetchStops\n          in: query\n          schema:\n            type: boolean\n          required: false\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n\n            Optional. Default is `false`. If set to `true`, the following stops are returned\n            for departures and the previous stops are returned for arrivals.\n\n        - name: pageCursor\n          in: query\n          required: false\n          description: |\n            Use the cursor to go to the next \"page\" of stop times.\n            Copy the cursor from the last response and keep the original request as is.\n            This will enable you to search for stop times in the next or previous time-window.\n          schema:\n            type: string\n\n        - name: withScheduledSkippedStops\n          in: query\n          required: false\n          description: Optional. Include stoptimes where passengers can not alight/board according to schedule.\n          schema:\n            type: boolean\n            default: false\n\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n        - name: withAlerts\n          in: query\n          required: false\n          description: Optional. Default is `true`. If set to `false`, alerts are omitted in the metadata of place for all stopTimes.\n          schema:\n            type: boolean\n            default: true\n\n      responses:\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: A list of departures/arrivals\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - stopTimes\n                  - place\n                  - previousPageCursor\n                  - nextPageCursor\n                properties:\n                  stopTimes:\n                    description: list of stop times\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/StopTime'\n                  place:\n                    description: metadata of the requested stop\n                    $ref: '#/components/schemas/Place'\n                  previousPageCursor:\n                    description: |\n                      Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page.\n                      The previous page is a set of stop times BEFORE the first stop time in the result.\n                    type: string\n                  nextPageCursor:\n                    description: |\n                      Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page.\n                      The next page is a set of stop times AFTER the last stop time in this result.\n                    type: string\n\n  /api/v5/map/trips:\n    get:\n      tags:\n        - map\n      operationId: trips\n      summary: |\n        Given a area frame (box defined by top right and bottom left corner) and a time frame,\n        it returns all trips and their respective shapes that operate in this area + time frame.\n        Trips are filtered by zoom level. On low zoom levels, only long distance trains will be shown\n        while on high zoom levels, also metros, buses and trams will be returned.\n      parameters:\n        - name: zoom\n          in: query\n          required: true\n          description: current zoom level\n          schema:\n            type: number\n        - name: min\n          in: query\n          required: true\n          description: latitude,longitude pair of the lower right coordinate\n          schema:\n            type: string\n        - name: max\n          in: query\n          required: true\n          description: latitude,longitude pair of the upper left coordinate\n          schema:\n            type: string\n        - name: startTime\n          in: query\n          required: true\n          description: start of the time window\n          schema:\n            type: string\n            format: date-time\n        - name: endTime\n          in: query\n          required: true\n          description: end if the time window\n          schema:\n            type: string\n            format: date-time\n        - name: precision\n          in: query\n          required: false\n          description: \"precision of returned polylines. Recommended to set based on zoom: `zoom >= 11 ? 5 : zoom >= 8 ? 4 : zoom >= 5 ? 3 : 2`\"\n          schema:\n            type: number\n            minimum: 0\n            maximum: 6\n            default: 5\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n      responses:\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: a list of trips\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/TripSegment'\n\n  /api/v1/map/initial:\n    get:\n      tags:\n        - map\n      operationId: initial\n      summary: initial location to view the map at after loading based on where public transport should be visible\n      responses:\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: latitude, longitude, zoom level to set the map to, and routing options configuration and limits\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - lat\n                  - lon\n                  - zoom\n                  - serverConfig\n                properties:\n                  lat:\n                    description: latitude\n                    type: number\n                  lon:\n                    description: longitude\n                    type: number\n                  zoom:\n                    description: zoom level\n                    type: number\n                  serverConfig:\n                    $ref: '#/components/schemas/ServerConfig'\n\n  /api/v1/map/stops:\n    get:\n      tags:\n        - map\n      summary: Get all stops for a map section\n      operationId: stops\n      parameters:\n        - name: min\n          in: query\n          required: true\n          description: latitude,longitude pair of the lower right coordinate\n          schema:\n            type: string\n\n        - name: max\n          in: query\n          required: true\n          description: latitude,longitude pair of the upper left coordinate\n          schema:\n            type: string\n\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n\n      responses:\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: array of stop places in the selected map section\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Place'\n\n  /api/v1/map/levels:\n    get:\n      tags:\n        - map\n      summary: Get all available levels for a map section\n      operationId: levels\n      parameters:\n        - name: min\n          in: query\n          required: true\n          description: latitude,longitude pair of the lower right coordinate\n          schema:\n            type: string\n        - name: max\n          in: query\n          required: true\n          description: latitude,longitude pair of the upper left coordinate\n          schema:\n            type: string\n      responses:\n        '500':\n          description: Internal Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: array of available levels\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: number\n\n  /api/experimental/map/routes:\n    get:\n      tags:\n        - map\n      operationId: routes\n      summary: |\n        Given an area frame (box defined by the top-right and bottom-left corners),\n        it returns all routes and their respective shapes that operate within this area.\n        Routes are filtered by zoom level. On low zoom levels, only long distance trains will be shown\n        while on high zoom levels, also metros, buses and trams will be returned.\n      parameters:\n        - name: zoom\n          in: query\n          required: true\n          description: current zoom level\n          schema:\n            type: number\n        - name: min\n          in: query\n          required: true\n          description: latitude,longitude pair of the lower right coordinate\n          schema:\n            type: string\n        - name: max\n          in: query\n          required: true\n          description: latitude,longitude pair of the upper left coordinate\n          schema:\n            type: string\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n      responses:\n        '422':\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: Server Error\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: a list of routes in the area\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - routes\n                  - polylines\n                  - stops\n                  - zoomFiltered\n                properties:\n                  routes:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RouteInfo'\n                  polylines:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RoutePolyline'\n                  stops:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/Place'\n                  zoomFiltered:\n                    type: boolean\n                    description: |\n                      Indicates whether some routes were filtered out due to\n                      the zoom level.\n\n  /api/experimental/map/route-details:\n    get:\n      tags:\n        - map\n      operationId: routeDetails\n      summary: |\n        Returns the full data for a single route, including all stops and\n        polyline segments.\n      parameters:\n        - name: routeIdx\n          in: query\n          required: true\n          description: Internal route index\n          schema:\n            type: integer\n        - name: language\n          in: query\n          required: false\n          description: |\n            language tags as used in OpenStreetMap / GTFS\n            (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n      responses:\n        \"422\":\n          description: Unprocessable Entity\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"400\":\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"404\":\n          description: Not Found\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"500\":\n          description: Server Error\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Error\"\n        \"200\":\n          description: full data for a single route\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - routes\n                  - polylines\n                  - stops\n                  - zoomFiltered\n                properties:\n                  routes:\n                    type: array\n                    items:\n                      $ref: \"#/components/schemas/RouteInfo\"\n                  polylines:\n                    type: array\n                    items:\n                      $ref: \"#/components/schemas/RoutePolyline\"\n                  stops:\n                    type: array\n                    items:\n                      $ref: \"#/components/schemas/Place\"\n                  zoomFiltered:\n                    type: boolean\n                    description: Always false for this endpoint.\n\n  /api/v1/rentals:\n    get:\n      tags:\n        - map\n      summary: |\n        Get a list of rental providers or all rental stations and vehicles for\n        a map section or provider\n      operationId: rentals\n      description: |\n        Various options to filter the providers, stations and vehicles are\n        available. If none of these filters are provided, a list of all\n        available rental providers is returned without any station, vehicle or\n        zone data.\n        \n        At least one of the following filters must be provided to retrieve\n        station, vehicle and zone data:\n        \n        - A bounding box defined by the `min` and `max` parameters\n        - A circle defined by the `point` and `radius` parameters\n        - A list of provider groups via the `providerGroups` parameter\n        - A list of providers via the `providers` parameter\n        \n        Only data that matches all the provided filters is returned.\n        \n        Provide the `withProviders=false` parameter to retrieve only provider\n        groups if detailed feed information is not required.\n      parameters:\n        - name: min\n          in: query\n          required: false\n          description: latitude,longitude pair of the lower right coordinate\n          schema:\n            type: string\n        - name: max\n          in: query\n          required: false\n          description: latitude,longitude pair of the upper left coordinate\n          schema:\n            type: string\n        - name: point\n          in: query\n          required: false\n          description: |\n            \\`latitude,longitude[,level]\\` tuple with\n            - latitude and longitude in degrees\n            - (optional) level: the OSM level (ignored, for compatibility reasons)\n\n            OR\n\n            stop id\n          schema:\n            type: string\n        - name: radius\n          in: query\n          schema:\n            type: integer\n          required: false\n          description: |\n            Radius around `point` in meters.\n        - name: providerGroups\n          in: query\n          required: false\n          description: |\n            A list of rental provider groups to return.\n            If both `providerGroups` and `providers` are empty/not specified,\n            all providers in the map section are returned.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n        - name: providers\n          in: query\n          required: false\n          description: |\n            A list of rental providers to return.\n            If both `providerGroups` and `providers` are empty/not specified,\n            all providers in the map section are returned.\n          schema:\n            type: array\n            items:\n              type: string\n          explode: false\n        - name: withProviders\n          in: query\n          required: false\n          description: |\n            Optional. Include providers in output. If false, only provider\n            groups are returned.\n            \n            Also affects the providers list for each provider group.\n          schema:\n            type: boolean\n            default: true\n        - name: withStations\n          in: query\n          required: false\n          description: Optional. Include stations in output (requires at least min+max or providers filter).\n          schema:\n            type: boolean\n            default: true\n        - name: withVehicles\n          in: query\n          required: false\n          description: Optional. Include free-floating vehicles in output (requires at least min+max or providers filter).\n          schema:\n            type: boolean\n            default: true\n        - name: withZones\n          in: query\n          required: false\n          description: Optional. Include geofencing zones in output (requires at least min+max or providers filter).\n          schema:\n            type: boolean\n            default: true\n      responses:\n        '400':\n          description: Bad Request\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '200':\n          description: Rentals in the map section or for the given providers\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - providerGroups\n                  - providers\n                  - stations\n                  - vehicles\n                  - zones\n                properties:\n                  providerGroups:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RentalProviderGroup'\n                  providers:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RentalProvider'\n                  stations:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RentalStation'\n                  vehicles:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RentalVehicle'\n                  zones:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/RentalZone'\n\n  /api/debug/transfers:\n    get:\n      tags:\n        - debug\n      summary: Prints all transfers of a timetable location (track, bus stop, etc.)\n      operationId: transfers\n      parameters:\n        - name: id\n          in: query\n          required: true\n          description: location id\n          schema:\n            type: string\n      responses:\n        '200':\n          description: list of outgoing transfers of this location\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - place\n                  - root\n                  - equivalences\n                  - hasFootTransfers\n                  - hasWheelchairTransfers\n                  - hasCarTransfers\n                  - transfers\n                properties:\n                  place:\n                    $ref: '#/components/schemas/Place'\n                  root:\n                    $ref: '#/components/schemas/Place'\n                  equivalences:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/Place'\n                  hasFootTransfers:\n                    type: boolean\n                    description: true if the server has foot transfers computed\n                  hasWheelchairTransfers:\n                    type: boolean\n                    description: true if the server has wheelchair transfers computed\n                  hasCarTransfers:\n                    type: boolean\n                    description: true if the server has car transfers computed\n                  transfers:\n                    description: all outgoing transfers of this location\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/Transfer'\n\ncomponents:\n  schemas:\n    AlertCause:\n      description: Cause of this alert.\n      type: string\n      enum:\n        - UNKNOWN_CAUSE\n        - OTHER_CAUSE\n        - TECHNICAL_PROBLEM\n        - STRIKE\n        - DEMONSTRATION\n        - ACCIDENT\n        - HOLIDAY\n        - WEATHER\n        - MAINTENANCE\n        - CONSTRUCTION\n        - POLICE_ACTIVITY\n        - MEDICAL_EMERGENCY\n\n    AlertEffect:\n      description: The effect of this problem on the affected entity.\n      type: string\n      enum:\n        - NO_SERVICE\n        - REDUCED_SERVICE\n        - SIGNIFICANT_DELAYS\n        - DETOUR\n        - ADDITIONAL_SERVICE\n        - MODIFIED_SERVICE\n        - OTHER_EFFECT\n        - UNKNOWN_EFFECT\n        - STOP_MOVED\n        - NO_EFFECT\n        - ACCESSIBILITY_ISSUE\n\n    AlertSeverityLevel:\n      description: The severity of the alert.\n      type: string\n      enum:\n        - UNKNOWN_SEVERITY\n        - INFO\n        - WARNING\n        - SEVERE\n\n    TimeRange:\n      description: |\n        A time interval.\n        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.\n      type: object\n      required:\n        - start\n        - end\n      properties:\n        start:\n          description: |\n            If missing, the interval starts at minus infinity.\n            If a TimeRange is provided, either start or end must be provided - both fields cannot be empty.\n          type: string\n          format: date-time\n        end:\n          description: |\n            If missing, the interval ends at plus infinity.\n            If a TimeRange is provided, either start or end must be provided - both fields cannot be empty.\n          type: string\n          format: date-time\n\n    Alert:\n      description: An alert, indicating some sort of incident in the public transit network.\n      type: object\n      required:\n        - headerText\n        - descriptionText\n      properties:\n        code:\n          type: string\n          description: Attribute or notice code (e.g. for HRDF or NeTEx)\n        communicationPeriod:\n          description: |\n            Time when the alert should be shown to the user.\n            If missing, the alert will be shown as long as it appears in the feed.\n            If multiple ranges are given, the alert will be shown during all of them.\n          type: array\n          items:\n            $ref: '#/components/schemas/TimeRange'\n        impactPeriod:\n          description: Time when the services are affected by the disruption mentioned in the alert.\n          type: array\n          items:\n            $ref: '#/components/schemas/TimeRange'\n        cause:\n          $ref: '#/components/schemas/AlertCause'\n        causeDetail:\n          type: string\n          description: |\n            Description of the cause of the alert that allows for agency-specific language;\n            more specific than the Cause.\n        effect:\n          $ref: '#/components/schemas/AlertEffect'\n        effectDetail:\n          type: string\n          description: |\n            Description of the effect of the alert that allows for agency-specific language;\n            more specific than the Effect.\n        url:\n          type: string\n          description: The URL which provides additional information about the alert.\n        headerText:\n          type: string\n          description: |\n            Header for the alert. This plain-text string will be highlighted, for example in boldface.\n        descriptionText:\n          type: string\n          description: |\n            Description for the alert.\n            This plain-text string will be formatted as the body of the alert (or shown on an explicit \"expand\" request by the user).\n            The information in the description should add to the information of the header.\n        ttsHeaderText:\n          type: string\n          description: |\n            Text containing the alert's header to be used for text-to-speech implementations.\n            This field is the text-to-speech version of header_text.\n            It should contain the same information as headerText but formatted such that it can read as text-to-speech\n            (for example, abbreviations removed, numbers spelled out, etc.)\n        ttsDescriptionText:\n          type: string\n          description: |\n            Text containing a description for the alert to be used for text-to-speech implementations.\n            This field is the text-to-speech version of description_text.\n            It should contain the same information as description_text but formatted such that it can be read as text-to-speech\n            (for example, abbreviations removed, numbers spelled out, etc.)\n        severityLevel:\n          description: Severity of the alert.\n          $ref: '#/components/schemas/AlertSeverityLevel'\n        imageUrl:\n          description: String containing an URL linking to an image.\n          type: string\n        imageMediaType:\n          description: |\n            IANA media type as to specify the type of image to be displayed. The type must start with \"image/\"\n          type: string\n        imageAlternativeText:\n          description: |\n            Text describing the appearance of the linked image in the image field\n            (e.g., in case the image can't be displayed or the user can't see the image for accessibility reasons).\n            See the HTML spec for alt image text.\n          type: string\n\n    Duration:\n      description: Object containing duration if a path was found or none if no path was found\n      type: object\n      properties:\n        duration:\n          type: number\n          description: duration in seconds if a path was found, otherwise missing\n          minimum: 0.0\n        distance:\n          type: number\n          description: distance in meters if a path was found and distance computation was requested, otherwise missing\n          minimum: 0.0\n\n    ParetoSetEntry:\n      description: Object containing a single element of a ParetoSet\n      type: object\n      required:\n        - duration\n        - transfers\n      properties:\n        duration:\n          type: number\n          description: |\n            duration in seconds for the the best solution using `transfer` transfers\n\n            Notice that the resolution is currently in minutes, because of implementation details\n          minimum: 0.0\n        transfers:\n          description: |\n            The minimal number of transfers required to arrive within `duration` seconds\n\n            transfers=0: Direct transit connecion without any transfers\n            transfers=1: Transit connection with 1 transfer\n          type: integer\n          minimum: 0\n\n    ParetoSet:\n      description: Pareto set of optimal transit solutions\n      type: array\n      items:\n        $ref: '#/components/schemas/ParetoSetEntry'\n\n    OneToManyIntermodalResponse:\n      description: Object containing the optimal street and transit durations for One-to-Many routing\n      type: object\n      properties:\n        street_durations:\n          description: |\n            Fastest durations for street routing\n            The order of the items corresponds to the order of the `many` locations\n            If no street routed connection is found, the corresponding `Duration` will be empty\n          type: array\n          items:\n            $ref: '#/components/schemas/Duration'\n        transit_durations:\n          description: |\n            Pareto optimal solutions\n            The order of the items corresponds to the order of the `many` locations\n            If no connection using transits is found, the corresponding `ParetoSet` will be empty\n          type: array\n          items:\n            $ref: '#/components/schemas/ParetoSet'\n\n    Area:\n      description: Administrative area\n      type: object\n      required:\n        - name\n        - adminLevel\n        - matched\n      properties:\n        name:\n          type: string\n          description: Name of the area\n        adminLevel:\n          type: number\n          description: |\n            [OpenStreetMap `admin_level`](https://wiki.openstreetmap.org/wiki/Key:admin_level)\n            of the area\n        matched:\n          type: boolean\n          description: Whether this area was matched by the input text\n        unique:\n          type: boolean\n          description: |\n            Set for the first area after the `default` area that distinguishes areas\n            if the match is ambiguous regarding (`default` area + place name / street [+ house number]).\n        default:\n          type: boolean\n          description: Whether this area should be displayed as default area (area with admin level closest 7)\n\n    Token:\n      description: Matched token range (from index, length)\n      type: array\n      minItems: 2\n      maxItems: 2\n      items:\n        type: number\n\n    LocationType:\n      description: location type\n      type: string\n      enum:\n        - ADDRESS\n        - PLACE\n        - STOP\n\n    Mode:\n      description: |\n        # Street modes\n        \n          - `WALK`\n          - `BIKE`\n          - `RENTAL` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs.\n          - `CAR`\n          - `CAR_PARKING` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs.\n          - `CAR_DROPOFF` Experimental. Expect unannounced breaking changes (without version bumps) for all perameters and returned structs.\n          - `ODM` on-demand taxis from the Prima+ÖV Project\n          - `RIDE_SHARING` ride sharing from the Prima+ÖV Project\n          - `FLEX` flexible transports\n\n        # Transit modes\n\n          - `TRANSIT`: translates to `TRAM,FERRY,AIRPLANE,BUS,COACH,RAIL,ODM,FUNICULAR,AERIAL_LIFT,OTHER`\n          - `TRAM`: trams\n          - `SUBWAY`: subway trains (Paris Metro, London Underground, but also NYC Subway, Hamburger Hochbahn, and other non-underground services)\n          - `FERRY`: ferries\n          - `AIRPLANE`: airline flights\n          - `BUS`: short distance buses (does not include `COACH`)\n          - `COACH`: long distance buses (does not include `BUS`)\n          - `RAIL`: translates to `HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN,SUBWAY`\n          - `HIGHSPEED_RAIL`: long distance high speed trains (e.g. TGV)\n          - `LONG_DISTANCE`: long distance inter city trains\n          - `NIGHT_RAIL`: long distance night trains\n          - `REGIONAL_FAST_RAIL`: deprecated, `REGIONAL_RAIL` will be used\n          - `REGIONAL_RAIL`: regional train\n          - `SUBURBAN`: suburban trains (e.g. S-Bahn, RER, Elizabeth Line, ...)\n          - `ODM`: demand responsive transport\n          - `FUNICULAR`: Funicular. Any rail system designed for steep inclines.\n          - `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.\n          - `AREAL_LIFT`: deprecated\n          - `METRO`: deprecated\n          - `CABLE_CAR`: deprecated\n      type: string\n      enum:\n        # === Street ===\n        - WALK\n        - BIKE\n        - RENTAL\n        - CAR\n        - CAR_PARKING\n        - CAR_DROPOFF\n        - ODM  # Transit | Street\n        - RIDE_SHARING  # Transit | Street\n        - FLEX\n        - DEBUG_BUS_ROUTE\n        - DEBUG_RAILWAY_ROUTE\n        - DEBUG_FERRY_ROUTE\n        # === Transit ===\n        - TRANSIT\n        - TRAM\n        - SUBWAY\n        - FERRY\n        - AIRPLANE\n        - BUS\n        - COACH\n        - RAIL\n        - HIGHSPEED_RAIL\n        - LONG_DISTANCE\n        - NIGHT_RAIL\n        - REGIONAL_FAST_RAIL\n        - REGIONAL_RAIL\n        - SUBURBAN\n        - FUNICULAR\n        - AERIAL_LIFT\n        - OTHER\n        - AREAL_LIFT\n        - METRO\n        - CABLE_CAR\n\n    Match:\n      description: GeoCoding match\n      type: object\n      required:\n        - type\n        - name\n        - id\n        - lat\n        - lon\n        - tokens\n        - areas\n        - score\n      properties:\n        type:\n          $ref: '#/components/schemas/LocationType'\n        category:\n          description: |\n            Experimental. Type categories might be adjusted.\n            \n            For OSM stop locations: the amenity type based on\n            https://wiki.openstreetmap.org/wiki/OpenStreetMap_Carto/Symbols\n          type: string\n        tokens:\n          description: list of non-overlapping tokens that were matched\n          type: array\n          items:\n            $ref: '#/components/schemas/Token'\n        name:\n          description: name of the location (transit stop / PoI / address)\n          type: string\n        id:\n          description: unique ID of the location\n          type: string\n        lat:\n          description: latitude\n          type: number\n        lon:\n          description: longitude\n          type: number\n        level:\n          description: |\n            level according to OpenStreetMap\n            (at the moment only for public transport)\n          type: number\n        street:\n          description: street name\n          type: string\n        houseNumber:\n          description: house number\n          type: string\n        country:\n          description: ISO3166-1 country code from OpenStreetMap\n          type: string\n        zip:\n          description: zip code\n          type: string\n        tz:\n          description: timezone name (e.g. \"Europe/Berlin\")\n          type: string\n        areas:\n          description: list of areas\n          type: array\n          items:\n            $ref: '#/components/schemas/Area'\n        score:\n          description: score according to the internal scoring system (the scoring algorithm might change in the future)\n          type: number\n        modes:\n          description: available transport modes for stops\n          type: array\n          items:\n            $ref: \"#/components/schemas/Mode\"\n        importance:\n          description: importance of a stop, normalized from [0, 1]\n          type: number\n\n    ElevationCosts:\n      description: |\n        Different elevation cost profiles for street routing.\n        Using a elevation cost profile will prefer routes with a smaller incline and smaller difference in elevation, even if the routed way is longer.\n\n        - `NONE`: Ignore elevation data for routing. This is the default behavior\n        - `LOW`: Add a low penalty for inclines. This will favor longer paths, if the elevation increase and incline are smaller.\n        - `HIGH`: Add a high penalty for inclines. This will favor even longer paths, if the elevation increase and incline are smaller.\n      type: string\n      enum:\n        - NONE\n        - LOW\n        - HIGH\n\n    PedestrianProfile:\n      description: Different accessibility profiles for pedestrians.\n      type: string\n      enum:\n        - FOOT\n        - WHEELCHAIR\n\n    PedestrianSpeed:\n      description: Average speed for pedestrian routing in meters per second\n      type: number\n\n    CyclingSpeed:\n      description: Average speed for bike routing in meters per second\n      type: number\n\n    VertexType:\n      type: string\n      description: |\n        - `NORMAL` - latitude / longitude coordinate or address\n        - `BIKESHARE` - bike sharing station\n        - `TRANSIT` - transit stop\n      enum:\n        - NORMAL\n        - BIKESHARE\n        - TRANSIT\n\n    PickupDropoffType:\n      type: string\n      description: |\n        - `NORMAL` - entry/exit is possible normally\n        - `NOT_ALLOWED` - entry/exit is not allowed\n      enum:\n        - NORMAL\n        - NOT_ALLOWED\n\n    Place:\n      type: object\n      required:\n        - name\n        - lat\n        - lon\n        - level\n      properties:\n        name:\n          description: name of the transit stop / PoI / address\n          type: string\n        stopId:\n          description: The ID of the stop. This is often something that users don't care about.\n          type: string\n        parentId:\n          description: If it's not a root stop, this field contains the `stopId` of the parent stop.\n          type: string\n        importance:\n          description: The importance of the stop between 0-1.\n          type: number\n        lat:\n          description: latitude\n          type: number\n        lon:\n          description: longitude\n          type: number\n        level:\n          description: level according to OpenStreetMap\n          type: number\n        tz:\n          description: timezone name (e.g. \"Europe/Berlin\")\n          type: string\n        arrival:\n          description: arrival time\n          type: string\n          format: date-time\n        departure:\n          description: departure time\n          type: string\n          format: date-time\n        scheduledArrival:\n          description: scheduled arrival time\n          type: string\n          format: date-time\n        scheduledDeparture:\n          description: scheduled departure time\n          type: string\n          format: date-time\n        scheduledTrack:\n          description: scheduled track from the static schedule timetable dataset\n          type: string\n        track:\n          description: |\n            The current track/platform information, updated with real-time updates if available. \n            Can be missing if neither real-time updates nor the schedule timetable contains track information.\n          type: string\n        description:\n          description: description of the location that provides more detailed information\n          type: string\n        vertexType:\n          $ref: '#/components/schemas/VertexType'\n        pickupType:\n          description: Type of pickup. It could be disallowed due to schedule, skipped stops or cancellations.\n          $ref: '#/components/schemas/PickupDropoffType'\n        dropoffType:\n          description: Type of dropoff. It could be disallowed due to schedule, skipped stops or cancellations.\n          $ref: '#/components/schemas/PickupDropoffType'\n        cancelled:\n          description: Whether this stop is cancelled due to the realtime situation.\n          type: boolean\n        alerts:\n          description: Alerts for this stop.\n          type: array\n          items:\n            $ref: '#/components/schemas/Alert'\n        flex:\n          description: for `FLEX` transports, the flex location area or location group name\n          type: string\n        flexId:\n          description: for `FLEX` transports, the flex location area ID or location group ID\n          type: string\n        flexStartPickupDropOffWindow:\n          description: Time that on-demand service becomes available\n          type: string\n          format: date-time\n        flexEndPickupDropOffWindow:\n          description: Time that on-demand service ends\n          type: string\n          format: date-time\n        modes:\n          description: available transport modes for stops\n          type: array\n          items:\n            $ref: \"#/components/schemas/Mode\"\n\n    ReachablePlace:\n      description: Place reachable by One-to-All\n      type: object\n      properties:\n        place:\n          $ref: \"#/components/schemas/Place\"\n          description: Place reached by One-to-All\n        duration:\n          type: integer\n          description: Total travel duration\n        k:\n          type: integer\n          description: |\n            k is the smallest number, for which a journey with the shortest duration and at most k-1 transfers exist.\n            You can think of k as the number of connections used.\n\n            In more detail:\n\n            k=0: No connection, e.g. for the one location\n            k=1: Direct connection\n            k=2: Connection with 1 transfer\n\n    Reachable:\n      description: Object containing all reachable places by One-to-All search\n      type: object\n      properties:\n        one:\n          $ref: \"#/components/schemas/Place\"\n          description: One location used in One-to-All search\n        all:\n          description: List of locations reachable by One-to-All\n          type: array\n          items:\n            $ref: \"#/components/schemas/ReachablePlace\"\n\n    StopTime:\n      description: departure or arrival event at a stop\n      type: object\n      required:\n        - place\n        - mode\n        - realTime\n        - headsign\n        - tripFrom\n        - tripTo\n        - agencyId\n        - agencyName\n        - agencyUrl\n        - tripId\n        - routeId\n        - directionId\n        - routeShortName\n        - routeLongName\n        - tripShortName\n        - displayName\n        - pickupDropoffType\n        - cancelled\n        - tripCancelled\n        - source\n      properties:\n        place:\n          $ref: '#/components/schemas/Place'\n          description: information about the stop place and time\n        mode:\n          $ref: '#/components/schemas/Mode'\n          description: Transport mode for this leg\n        realTime:\n          description: Whether there is real-time data about this leg\n          type: boolean\n        headsign:\n          description: |\n            The headsign of the bus or train being used.\n            For non-transit legs, null\n          type: string\n        tripFrom:\n          description: first stop of this trip\n          $ref: '#/components/schemas/Place'\n        tripTo:\n          description: final stop of this trip\n          $ref: '#/components/schemas/Place'\n        agencyId:\n          type: string\n        agencyName:\n          type: string\n        agencyUrl:\n          type: string\n        routeId:\n          type: string\n        routeUrl:\n          type: string\n        directionId:\n          type: string\n        routeColor:\n          type: string\n        routeTextColor:\n          type: string\n        tripId:\n          type: string\n        routeType:\n          type: integer\n        routeShortName:\n          type: string\n        routeLongName:\n          type: string\n        tripShortName:\n          type: string\n        displayName:\n          type: string\n        previousStops:\n          type: array\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n\n            Stops on the trips before this stop. Returned only if `fetchStop` and `arriveBy` are `true`.\n          items:\n            $ref: \"#/components/schemas/Place\"\n        nextStops:\n          type: array\n          description: |\n            Experimental. Expect unannounced breaking changes (without version bumps).\n\n            Stops on the trips after this stop. Returned only if `fetchStop` is `true` and `arriveBy` is `false`.\n          items:\n            $ref: \"#/components/schemas/Place\"\n        pickupDropoffType:\n          description: Type of pickup (for departures) or dropoff (for arrivals), may be disallowed either due to schedule, skipped stops or cancellations\n          $ref: '#/components/schemas/PickupDropoffType'\n        cancelled:\n          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).\n          type: boolean\n        tripCancelled:\n          description: Whether the entire trip is cancelled due to the realtime situation.\n          type: boolean\n        source:\n          description: Filename and line number where this trip is from\n          type: string\n\n    TripInfo:\n      description: trip id and name\n      type: object\n      required:\n        - tripId\n      properties:\n        tripId:\n          description: trip ID (dataset trip id prefixed with the dataset tag)\n          type: string\n        routeShortName:\n          description: trip display name (api version < 4)\n          type: string\n        displayName:\n          description: trip display name (api version >= 4)\n          type: string\n\n    TripSegment:\n      description: trip segment between two stops to show a trip on a map\n      type: object\n      required:\n        - trips\n        - mode\n        - distance\n        - from\n        - to\n        - departure\n        - arrival\n        - scheduledArrival\n        - scheduledDeparture\n        - realTime\n        - polyline\n      properties:\n        trips:\n          type: array\n          items:\n            $ref: '#/components/schemas/TripInfo'\n        routeColor:\n          type: string\n        mode:\n          $ref: '#/components/schemas/Mode'\n          description: Transport mode for this leg\n        distance:\n          type: number\n          description: distance in meters\n        from:\n          $ref: '#/components/schemas/Place'\n        to:\n          $ref: '#/components/schemas/Place'\n        departure:\n          description: departure time\n          type: string\n          format: date-time\n        arrival:\n          description: arrival time\n          type: string\n          format: date-time\n        scheduledDeparture:\n          description: scheduled departure time\n          type: string\n          format: date-time\n        scheduledArrival:\n          description: scheduled arrival time\n          type: string\n          format: date-time\n        realTime:\n          description: Whether there is real-time data about this leg\n          type: boolean\n        polyline:\n          description: Google polyline encoded coordinate sequence (with precision 5) where the trip travels on this segment.\n          type: string\n\n    Direction:\n      type: string\n      enum:\n        - DEPART\n        - HARD_LEFT\n        - LEFT\n        - SLIGHTLY_LEFT\n        - CONTINUE\n        - SLIGHTLY_RIGHT\n        - RIGHT\n        - HARD_RIGHT\n        - CIRCLE_CLOCKWISE\n        - CIRCLE_COUNTERCLOCKWISE\n        - STAIRS\n        - ELEVATOR\n        - UTURN_LEFT\n        - UTURN_RIGHT\n\n    EncodedPolyline:\n      type: object\n      required:\n        - points\n        - precision\n        - length\n      properties:\n        points:\n          description: The encoded points of the polyline using the Google polyline encoding.\n          type: string\n        precision:\n          description: |\n            The precision of the returned polyline (7 for /v1, 6 for /v2)\n            Be aware that with precision 7, coordinates with |longitude| > 107.37 are undefined/will overflow.\n          type: integer\n        length:\n          description: The number of points in the string\n          type: integer\n          minimum: 0\n\n    StepInstruction:\n      type: object\n      required:\n        - fromLevel\n        - toLevel\n        - polyline\n        - relativeDirection\n        - distance\n        - streetName\n        - exit\n        - stayOn\n        - area\n      properties:\n        relativeDirection:\n          $ref: '#/components/schemas/Direction'\n        distance:\n          description: The distance in meters that this step takes.\n          type: number\n        fromLevel:\n          description: level where this segment starts, based on OpenStreetMap data\n          type: number\n        toLevel:\n          description: level where this segment starts, based on OpenStreetMap data\n          type: number\n        osmWay:\n          description: OpenStreetMap way index\n          type: integer\n        polyline:\n          $ref: '#/components/schemas/EncodedPolyline'\n        streetName:\n          description: The name of the street.\n          type: string\n        exit:\n          description: |\n            Not implemented!\n            When exiting a highway or traffic circle, the exit name/number.\n          type: string\n        stayOn:\n          description: |\n            Not implemented!\n            Indicates whether or not a street changes direction at an intersection.\n          type: boolean\n        area:\n          description: |\n            Not implemented!\n            This step is on an open area, such as a plaza or train platform,\n            and thus the directions should say something like \"cross\"\n          type: boolean\n        toll:\n          description: Indicates that a fee must be paid by general traffic to use a road, road bridge or road tunnel.\n          type: boolean\n        accessRestriction:\n          description: |\n            Experimental. Indicates whether access to this part of the route is restricted.\n            See: https://wiki.openstreetmap.org/wiki/Conditional_restrictions\n          type: string\n        elevationUp:\n          type: integer\n          description: incline in meters across this path segment\n        elevationDown:\n          type: integer\n          description: decline in meters across this path segment\n\n    RentalFormFactor:\n      type: string\n      enum:\n        - BICYCLE\n        - CARGO_BICYCLE\n        - CAR\n        - MOPED\n        - SCOOTER_STANDING\n        - SCOOTER_SEATED\n        - OTHER\n\n    RentalPropulsionType:\n      type: string\n      enum:\n        - HUMAN\n        - ELECTRIC_ASSIST\n        - ELECTRIC\n        - COMBUSTION\n        - COMBUSTION_DIESEL\n        - HYBRID\n        - PLUG_IN_HYBRID\n        - HYDROGEN_FUEL_CELL\n\n    RentalReturnConstraint:\n      type: string\n      enum:\n        - NONE\n        - ANY_STATION\n        - ROUNDTRIP_STATION\n\n    Rental:\n      description: Vehicle rental\n      type: object\n      required:\n        - providerId\n        - providerGroupId\n        - systemId\n      properties:\n        providerId:\n          type: string\n          description: Rental provider ID\n        providerGroupId:\n          type: string\n          description: Rental provider group ID\n        systemId:\n          type: string\n          description: Vehicle share system ID\n        systemName:\n          type: string\n          description: Vehicle share system name\n        url:\n          type: string\n          description: URL of the vehicle share system\n        color:\n          type: string\n          description: |\n            Color associated with this provider, in hexadecimal RGB format\n            (e.g. \"#FF0000\" for red). Can be empty.\n        stationName:\n          type: string\n          description: Name of the station\n        fromStationName:\n          type: string\n          description: Name of the station where the vehicle is picked up (empty for free floating vehicles)\n        toStationName:\n          type: string\n          description: Name of the station where the vehicle is returned (empty for free floating vehicles)\n        rentalUriAndroid:\n          type: string\n          description: Rental URI for Android (deep link to the specific station or vehicle)\n        rentalUriIOS:\n          type: string\n          description: Rental URI for iOS (deep link to the specific station or vehicle)\n        rentalUriWeb:\n          type: string\n          description: Rental URI for web (deep link to the specific station or vehicle)\n        formFactor:\n          $ref: '#/components/schemas/RentalFormFactor'\n        propulsionType:\n          $ref: '#/components/schemas/RentalPropulsionType'\n        returnConstraint:\n          $ref: '#/components/schemas/RentalReturnConstraint'\n\n    MultiPolygon:\n      type: array\n      description: |\n        A multi-polygon contains a number of polygons, each containing a number\n        of rings, which are encoded as polylines (with precision 6).\n        \n        For each polygon, the first ring is the outer ring, all subsequent rings\n        are inner rings (holes).\n      items: # polygons\n        type: array\n        items: # rings\n          $ref: '#/components/schemas/EncodedPolyline'\n\n    RentalZoneRestrictions:\n      type: object\n      required:\n        - vehicleTypeIdxs\n        - rideStartAllowed\n        - rideEndAllowed\n        - rideThroughAllowed\n      properties:\n        vehicleTypeIdxs:\n          type: array\n          description: |\n            List of vehicle types (as indices into the provider's vehicle types\n            array) to which these restrictions apply.\n            If empty, the restrictions apply to all vehicle types of the provider.\n          items:\n            type: integer\n        rideStartAllowed:\n          type: boolean\n          description: whether the ride is allowed to start in this zone\n        rideEndAllowed:\n          type: boolean\n          description: whether the ride is allowed to end in this zone\n        rideThroughAllowed:\n          type: boolean\n          description: whether the ride is allowed to pass through this zone\n        stationParking:\n          type: boolean\n          description: whether vehicles can only be parked at stations in this zone\n\n    RentalVehicleType:\n      type: object\n      required:\n        - id\n        - formFactor\n        - propulsionType\n        - returnConstraint\n        - returnConstraintGuessed\n      properties:\n        id:\n          type: string\n          description: Unique identifier of the vehicle type (unique within the provider)\n        name:\n          type: string\n          description: Public name of the vehicle type (can be empty)\n        formFactor:\n          $ref: '#/components/schemas/RentalFormFactor'\n        propulsionType:\n          $ref: '#/components/schemas/RentalPropulsionType'\n        returnConstraint:\n          $ref: '#/components/schemas/RentalReturnConstraint'\n        returnConstraintGuessed:\n          type: boolean\n          description: Whether the return constraint was guessed instead of being specified by the rental provider\n\n    RentalProvider:\n      type: object\n      required:\n        - id\n        - name\n        - groupId\n        - bbox\n        - vehicleTypes\n        - formFactors\n        - defaultRestrictions\n        - globalGeofencingRules\n      properties:\n        id:\n          type: string\n          description: Unique identifier of the rental provider\n        name:\n          type: string\n          description: Name of the provider to be displayed to customers\n        groupId:\n          type: string\n          description: Id of the rental provider group this provider belongs to\n        operator:\n          type: string\n          description: Name of the system operator\n        url:\n          type: string\n          description: URL of the vehicle share system\n        purchaseUrl:\n          type: string\n          description: URL where a customer can purchase a membership\n        color:\n          type: string\n          description: |\n            Color associated with this provider, in hexadecimal RGB format\n            (e.g. \"#FF0000\" for red). Can be empty.\n        bbox:\n          type: array\n          description: |\n            Bounding box of the area covered by this rental provider,\n            [west, south, east, north] as [lon, lat, lon, lat]\n          minItems: 4\n          maxItems: 4\n          items:\n            type: number\n        vehicleTypes:\n          type: array\n          items:\n            $ref: '#/components/schemas/RentalVehicleType'\n        formFactors:\n          type: array\n          description: List of form factors offered by this provider\n          items:\n            $ref: '#/components/schemas/RentalFormFactor'\n        defaultRestrictions:\n          $ref: '#/components/schemas/RentalZoneRestrictions'\n        globalGeofencingRules:\n          type: array\n          items:\n            $ref: '#/components/schemas/RentalZoneRestrictions'\n\n    RentalProviderGroup:\n      type: object\n      required:\n        - id\n        - name\n        - providers\n        - formFactors\n      properties:\n        id:\n          type: string\n          description: Unique identifier of the rental provider group\n        name:\n          type: string\n          description: Name of the provider group to be displayed to customers\n        color:\n          type: string\n          description: |\n            Color associated with this provider group, in hexadecimal RGB format\n            (e.g. \"#FF0000\" for red). Can be empty.\n        providers:\n          type: array\n          description: List of rental provider IDs that belong to this group\n          items:\n            type: string\n        formFactors:\n          type: array\n          description: List of form factors offered by this provider group\n          items:\n            $ref: '#/components/schemas/RentalFormFactor'\n\n    RentalStation:\n      type: object\n      required:\n        - id\n        - providerId\n        - providerGroupId\n        - name\n        - lat\n        - lon\n        - isRenting\n        - isReturning\n        - numVehiclesAvailable\n        - formFactors\n        - vehicleTypesAvailable\n        - vehicleDocksAvailable\n        - bbox\n      properties:\n        id:\n          type: string\n          description: Unique identifier of the rental station\n        providerId:\n          type: string\n          description: Unique identifier of the rental provider\n        providerGroupId:\n          type: string\n          description: Unique identifier of the rental provider group\n        name:\n          type: string\n          description: Public name of the station\n        lat:\n          description: latitude\n          type: number\n        lon:\n          description: longitude\n          type: number\n        address:\n          type: string\n          description: Address where the station is located\n        crossStreet:\n          type: string\n          description: Cross street or landmark where the station is located\n        rentalUriAndroid:\n          type: string\n          description: Rental URI for Android (deep link to the specific station)\n        rentalUriIOS:\n          type: string\n          description: Rental URI for iOS (deep link to the specific station)\n        rentalUriWeb:\n          type: string\n          description: Rental URI for web (deep link to the specific station)\n        isRenting:\n          type: boolean\n          description: true if vehicles can be rented from this station, false if it is temporarily out of service\n        isReturning:\n          type: boolean\n          description: true if vehicles can be returned to this station, false if it is temporarily out of service\n        numVehiclesAvailable:\n          type: integer\n          description: Number of vehicles available for rental at this station\n        formFactors:\n          type: array\n          description: List of form factors available for rental and/or return at this station\n          items:\n            $ref: '#/components/schemas/RentalFormFactor'\n        vehicleTypesAvailable:\n          type: object\n          description: List of vehicle types currently available at this station (vehicle type ID -> count)\n          additionalProperties:\n            type: integer\n        vehicleDocksAvailable:\n          type: object\n          description: List of vehicle docks currently available at this station (vehicle type ID -> count)\n          additionalProperties:\n            type: integer\n        stationArea:\n          $ref: '#/components/schemas/MultiPolygon'\n        bbox:\n          type: array\n          description: |\n            Bounding box of the area covered by this station,\n            [west, south, east, north] as [lon, lat, lon, lat]\n          minItems: 4\n          maxItems: 4\n          items:\n            type: number\n\n    RentalVehicle:\n      type: object\n      required:\n        - id\n        - providerId\n        - providerGroupId\n        - typeId\n        - lat\n        - lon\n        - formFactor\n        - propulsionType\n        - returnConstraint\n        - isReserved\n        - isDisabled\n      properties:\n        id:\n          type: string\n          description: Unique identifier of the rental vehicle\n        providerId:\n          type: string\n          description: Unique identifier of the rental provider\n        providerGroupId:\n          type: string\n          description: Unique identifier of the rental provider group\n        typeId:\n          type: string\n          description: Vehicle type ID (unique within the provider)\n        lat:\n          description: latitude\n          type: number\n        lon:\n          description: longitude\n          type: number\n        formFactor:\n          $ref: '#/components/schemas/RentalFormFactor'\n        propulsionType:\n          $ref: '#/components/schemas/RentalPropulsionType'\n        returnConstraint:\n          $ref: '#/components/schemas/RentalReturnConstraint'\n        stationId:\n          type: string\n          description: Station ID if the vehicle is currently at a station\n        homeStationId:\n          type: string\n          description: Station ID where the vehicle must be returned, if applicable\n        isReserved:\n          type: boolean\n          description: true if the vehicle is currently reserved by a customer, false otherwise\n        isDisabled:\n          type: boolean\n          description: true if the vehicle is out of service, false otherwise\n        rentalUriAndroid:\n          type: string\n          description: Rental URI for Android (deep link to the specific vehicle)\n        rentalUriIOS:\n          type: string\n          description: Rental URI for iOS (deep link to the specific vehicle)\n        rentalUriWeb:\n          type: string\n          description: Rental URI for web (deep link to the specific vehicle)\n\n    RentalZone:\n      type: object\n      required:\n        - providerId\n        - providerGroupId\n        - z\n        - bbox\n        - area\n        - rules\n      properties:\n        providerId:\n          type: string\n          description: Unique identifier of the rental provider\n        providerGroupId:\n          type: string\n          description: Unique identifier of the rental provider group\n        name:\n          type: string\n          description: Public name of the geofencing zone\n        z:\n          type: integer\n          description: Zone precedence / z-index (higher number = higher precedence)\n        bbox:\n          type: array\n          description: |\n            Bounding box of the area covered by this zone,\n            [west, south, east, north] as [lon, lat, lon, lat]\n          minItems: 4\n          maxItems: 4\n          items:\n            type: number\n        area:\n          $ref: '#/components/schemas/MultiPolygon'\n        rules:\n          type: array\n          items:\n            $ref: '#/components/schemas/RentalZoneRestrictions'\n\n    Category:\n      type: object\n      required:\n        - id\n        - name\n        - shortName\n      description: |\n        not available for GTFS datasets by default\n        For NeTEx it contains information about the vehicle category, e.g. IC/InterCity\n      properties:\n        id:\n          type: string\n        name:\n          type: string\n        shortName:\n          type: string\n\n    Leg:\n      type: object\n      required:\n        - mode\n        - startTime\n        - endTime\n        - scheduledStartTime\n        - scheduledEndTime\n        - realTime\n        - scheduled\n        - duration\n        - from\n        - to\n        - legGeometry\n      properties:\n        mode:\n          $ref: '#/components/schemas/Mode'\n          description: Transport mode for this leg\n        from:\n          $ref: '#/components/schemas/Place'\n        to:\n          $ref: '#/components/schemas/Place'\n        duration:\n          description: |\n            Leg duration in seconds\n\n            If leg is footpath:\n              The footpath duration is derived from the default footpath\n              duration using the query parameters `transferTimeFactor` and\n              `additionalTransferTime` as follows:\n              `leg.duration = defaultDuration * transferTimeFactor + additionalTransferTime.`\n              In case the defaultDuration is needed, it can be calculated by\n              `defaultDuration = (leg.duration - additionalTransferTime) / transferTimeFactor`.\n              Note that the default values are `transferTimeFactor = 1` and\n              `additionalTransferTime = 0` in case they are not explicitly\n              provided in the query.\n          type: integer\n        startTime:\n          type: string\n          format: date-time\n          description: leg departure time\n        endTime:\n          type: string\n          format: date-time\n          description: leg arrival time\n        scheduledStartTime:\n          type: string\n          format: date-time\n          description: scheduled leg departure time\n        scheduledEndTime:\n          type: string\n          format: date-time\n          description: scheduled leg arrival time\n        realTime:\n          description: Whether there is real-time data about this leg\n          type: boolean\n        scheduled:\n          description: |\n            Whether this leg was originally scheduled to run or is an additional service.\n            Scheduled times will equal realtime times in this case.\n          type: boolean\n        distance:\n          description: For non-transit legs the distance traveled while traversing this leg in meters.\n          type: number\n        interlineWithPreviousLeg:\n          description: For transit legs, if the rider should stay on the vehicle as it changes route names.\n          type: boolean\n        headsign:\n          description: |\n            For transit legs, the headsign of the bus or train being used.\n            For non-transit legs, null\n          type: string\n        tripFrom:\n          description: first stop of this trip\n          $ref: '#/components/schemas/Place'\n        tripTo:\n          description: final stop of this trip (can differ from headsign)\n          $ref: '#/components/schemas/Place'\n        category:\n          $ref: '#/components/schemas/Category'\n        routeId:\n          type: string\n        routeUrl:\n          type: string\n        directionId:\n          type: string\n        routeColor:\n          type: string\n        routeTextColor:\n          type: string\n        routeType:\n          type: integer\n        agencyName:\n          type: string\n        agencyUrl:\n          type: string\n        agencyId:\n          type: string\n        tripId:\n          type: string\n        routeShortName:\n          type: string\n        routeLongName:\n          type: string\n        tripShortName:\n          type: string\n        displayName:\n          type: string\n        cancelled:\n          description: Whether this trip is cancelled\n          type: boolean\n        source:\n          description: Filename and line number where this trip is from\n          type: string\n        intermediateStops:\n          description: |\n            For transit legs, intermediate stops between the Place where the leg originates\n            and the Place where the leg ends. For non-transit legs, null.\n          type: array\n          items:\n            $ref: \"#/components/schemas/Place\"\n        legGeometry:\n          description: |\n            Encoded geometry of the leg.\n            If detailed leg output is disabled, this is returned as an empty\n            polyline.\n          $ref: '#/components/schemas/EncodedPolyline'\n        steps:\n          description: |\n            A series of turn by turn instructions\n            used for walking, biking and driving.\n            This field is omitted if the request disables detailed leg output.\n          type: array\n          items:\n            $ref: '#/components/schemas/StepInstruction'\n        rental:\n          $ref: '#/components/schemas/Rental'\n        fareTransferIndex:\n          type: integer\n          description: |\n            Index into `Itinerary.fareTransfers` array\n            to identify which fare transfer this leg belongs to\n        effectiveFareLegIndex:\n          type: integer\n          description: |\n            Index into the `Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts` array\n            to identify which effective fare leg this itinerary leg belongs to\n        alerts:\n          description: Alerts for this stop.\n          type: array\n          items:\n            $ref: '#/components/schemas/Alert'\n        loopedCalendarSince:\n          description: |\n            If set, this attribute indicates that this trip has been expanded\n            beyond the feed end date (enabled by config flag `timetable.dataset.extend_calendar`)\n            by looping active weekdays, e.g. from calendar.txt in GTFS.\n          type: string\n          format: date-time\n        bikesAllowed:\n          description: |\n            Whether bikes can be carried on this leg.\n          type: boolean\n\n    RiderCategory:\n      type: object\n      required:\n        - riderCategoryName\n        - isDefaultFareCategory\n      properties:\n        riderCategoryName:\n          description: Rider category name as displayed to the rider.\n          type: string\n        isDefaultFareCategory:\n          description: Specifies if this category should be considered the default (i.e. the main category displayed to riders).\n          type: boolean\n        eligibilityUrl:\n          description: URL to a web page providing detailed information about the rider category and/or its eligibility criteria.\n          type: string\n\n    FareMediaType:\n      type: string\n      enum: [ \"NONE\", \"PAPER_TICKET\", \"TRANSIT_CARD\", \"CONTACTLESS_EMV\", \"MOBILE_APP\" ]\n      description: |\n        - `NONE`: No fare media involved (e.g., cash payment)\n        - `PAPER_TICKET`: Physical paper ticket\n        - `TRANSIT_CARD`: Physical transit card with stored value\n        - `CONTACTLESS_EMV`: cEMV (contactless payment)\n        - `MOBILE_APP`: Mobile app with virtual transit cards/passes\n\n    FareMedia:\n      type: object\n      required:\n        - fareMediaType\n      properties:\n        fareMediaName:\n          description: Name of the fare media. Required for transit cards and mobile apps.\n          type: string\n        fareMediaType:\n          description: The type of fare media.\n          $ref: '#/components/schemas/FareMediaType'\n\n    FareProduct:\n      type: object\n      required:\n        - name\n        - amount\n        - currency\n      properties:\n        name:\n          description: The name of the fare product as displayed to riders.\n          type: string\n        amount:\n          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.\n          type: number\n        currency:\n          description: ISO 4217 currency code. The currency of the cost of the fare product.\n          type: string\n        riderCategory:\n          $ref: '#/components/schemas/RiderCategory'\n        media:\n          $ref: '#/components/schemas/FareMedia'\n\n    FareTransferRule:\n      type: string\n      enum:\n        - A_AB\n        - A_AB_B\n        - AB\n\n    FareTransfer:\n      type: object\n      description: |\n        The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt\n\n        Terminology:\n          - **Leg**: An itinerary leg as described by the `Leg` type of this API description.\n          - **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*.\n          - **Fare Transfer**: A fare transfer groups two or more effective fare legs.\n          - **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer\n          - **B** is any *effective fare leg* following the first *effective fare leg* in this transfer\n          - **AB** are all changes between *effective fare legs* contained in this transfer\n\n        The fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer:\n          - 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.\n          - 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)\n          - 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.\n\n        An itinerary `Leg` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to.\n      required:\n        - effectiveFareLegProducts\n      properties:\n        rule:\n          $ref: '#/components/schemas/FareTransferRule'\n        transferProducts:\n          type: array\n          items:\n            $ref: '#/components/schemas/FareProduct'\n        effectiveFareLegProducts:\n          description: |\n            Lists all valid fare products for the effective fare legs.\n            This is an `array<array<FareProduct>>` where the inner array\n            lists all possible fare products that would cover this effective fare leg.\n            Each \"effective fare leg\" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc.\n            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`)\n            and the inner array as OR (you can choose which ticket to buy)\n\n          type: array\n          items:\n            type: array\n            items:\n              type: array\n              items:\n                $ref: '#/components/schemas/FareProduct'\n\n    Itinerary:\n      type: object\n      required:\n        - duration\n        - startTime\n        - endTime\n        - transfers\n        - legs\n      properties:\n        duration:\n          description: journey duration in seconds\n          type: integer\n        startTime:\n          type: string\n          format: date-time\n          description: journey departure time\n        endTime:\n          type: string\n          format: date-time\n          description: journey arrival time\n        transfers:\n          type: integer\n          description: The number of transfers this trip has.\n        legs:\n          description: Journey legs\n          type: array\n          items:\n            $ref: '#/components/schemas/Leg'\n        fareTransfers:\n          description: Fare information\n          type: array\n          items:\n            $ref: '#/components/schemas/FareTransfer'\n\n    Transfer:\n      description: transfer from one location to another\n      type: object\n      required:\n        - to\n      properties:\n        to:\n          $ref: '#/components/schemas/Place'\n        default:\n          type: number\n          description: |\n            optional; missing if the GTFS did not contain a transfer\n            transfer duration in minutes according to GTFS (+heuristics)\n        foot:\n          type: number\n          description: |\n            optional; missing if no path was found (timetable / osr)\n            transfer duration in minutes for the foot profile\n        footRouted:\n          type: number\n          description: |\n            optional; missing if no path was found with foot routing\n            transfer duration in minutes for the foot profile\n        wheelchair:\n          type: number\n          description: |\n            optional; missing if no path was found with the wheelchair profile \n            transfer duration in minutes for the wheelchair profile\n        wheelchairRouted:\n          type: number\n          description: |\n            optional; missing if no path was found with the wheelchair profile\n            transfer duration in minutes for the wheelchair profile\n        wheelchairUsesElevator:\n          type: boolean\n          description: |\n            optional; missing if no path was found with the wheelchair profile\n            true if the wheelchair path uses an elevator\n        car:\n          type: number\n          description: |\n            optional; missing if no path was found with car routing\n            transfer duration in minutes for the car profile\n    OneToManyParams:\n      type: object\n      required:\n        - one\n        - many\n        - mode\n        - max\n        - maxMatchingDistance\n        - arriveBy\n      properties:\n        one:\n          description: geo location as latitude;longitude\n          type: string\n        many:\n          description: |\n            geo locations as latitude;longitude,latitude;longitude,...\n\n            The number of accepted locations is limited by server config variable `onetomany_max_many`.\n          type: array\n          items:\n            type: string\n          explode: false\n        mode:\n          description: |\n            routing profile to use (currently supported: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`)\n          $ref: '#/components/schemas/Mode'\n        max:\n          description: maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`.\n          type: number\n        maxMatchingDistance:\n          description: maximum matching distance in meters to match geo coordinates to the street network\n          type: number\n        elevationCosts:\n          description: |\n            Optional. Default is `NONE`.\n\n            Set an elevation cost profile, to penalize routes with incline.\n            - `NONE`: No additional costs for elevations. This is the default behavior\n            - `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.\n            - `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.\n\n            As using an elevation costs profile will increase the travel duration,\n            routing through steep terrain may exceed the maximal allowed duration,\n            causing a location to appear unreachable.\n            Increasing the maximum travel time for these segments may resolve this issue.\n\n            Elevation cost profiles are currently used by following street modes:\n            - `BIKE`\n          $ref: '#/components/schemas/ElevationCosts'\n          default: NONE\n        arriveBy:\n          description: |\n            true = many to one\n            false = one to many\n          type: boolean\n        withDistance:\n          description: |\n            If true, the response includes the distance in meters\n            for each path. This requires path reconstruction and\n            may be slower than duration-only queries.\n          type: boolean\n          default: false\n    OneToManyIntermodalParams:\n      type: object\n      required:\n        - one\n        - many\n      properties:\n        one:\n          description: |\n            \\`latitude,longitude[,level]\\` tuple with\n            - latitude and longitude in degrees\n            - (optional) level: the OSM level (default: 0)\n\n            OR\n\n            stop id\n          type: string\n        many:\n          description: |\n            array of:\n\n            \\`latitude,longitude[,level]\\` tuple with\n            - latitude and longitude in degrees\n            - (optional) level: the OSM level (default: 0)\n\n            OR\n\n            stop id\n\n            The number of accepted locations is limited by server config variable `onetomany_max_many`.\n          type: array\n          items:\n            type: string\n          explode: false\n        time:\n          description: |\n            Optional. Defaults to the current time.\n\n            Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n          type: string\n          format: date-time\n        maxTravelTime:\n          description: |\n            The maximum travel time in minutes.\n            If not provided, the routing uses the value\n            hardcoded in the server which is usually quite high.\n\n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the least transfers) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n          type: integer\n        maxMatchingDistance:\n          description: maximum matching distance in meters to match geo coordinates to the street network\n          type: number\n          default: 25\n        arriveBy:\n          description: |\n            Optional. Defaults to false, i.e. one to many search\n\n            true = many to one\n            false = one to many\n          type: boolean\n          default: false\n        maxTransfers:\n          description: |\n            The maximum number of allowed transfers (i.e. interchanges between transit legs,\n            pre- and postTransit do not count as transfers).\n            `maxTransfers=0` searches for direct transit connections without any transfers.\n            If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n            send an empty `transitModes` parameter instead.\n\n            If not provided, the routing uses the server-side default value\n            which is hardcoded and very high to cover all use cases.\n\n            *Warning*: Use with care. Setting this too low can lead to\n            optimal (e.g. the fastest) journeys not being found.\n            If this value is too low to reach the destination at all,\n            it can lead to slow routing performance.\n          type: integer\n        minTransferTime:\n          description: |\n            Optional. Default is 0 minutes.\n\n            Minimum transfer time for each transfer in minutes.\n          type: integer\n          default: 0\n        additionalTransferTime:\n          description: |\n            Optional. Default is 0 minutes.\n\n            Additional transfer time reserved for each transfer in minutes.\n          type: integer\n          default: 0\n        transferTimeFactor:\n          description: |\n            Optional. Default is 1.0\n\n            Factor to multiply minimum required transfer times with.\n            Values smaller than 1.0 are not supported.\n          type: number\n          default: 1.0\n        useRoutedTransfers:\n          description: |\n            Optional. Default is `false`.\n\n            Whether to use transfers routed on OpenStreetMap data.\n          type: boolean\n          default: false\n        pedestrianProfile:\n          description: |\n            Optional. Default is `FOOT`.\n\n            Accessibility profile to use for pedestrian routing in transfers\n            between transit connections and the first and last mile respectively.\n          $ref: \"#/components/schemas/PedestrianProfile\"\n          default: FOOT\n        pedestrianSpeed:\n          description: |\n            Optional\n\n            Average speed for pedestrian routing.\n          $ref: \"#/components/schemas/PedestrianSpeed\"\n        cyclingSpeed:\n          description: |\n            Optional\n\n            Average speed for bike routing.\n          $ref: \"#/components/schemas/CyclingSpeed\"\n        elevationCosts:\n          description: |\n            Optional. Default is `NONE`.\n\n            Set an elevation cost profile, to penalize routes with incline.\n            - `NONE`: No additional costs for elevations. This is the default behavior\n            - `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.\n            - `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.\n\n            As using an elevation costs profile will increase the travel duration,\n            routing through steep terrain may exceed the maximal allowed duration,\n            causing a location to appear unreachable.\n            Increasing the maximum travel time for these segments may resolve this issue.\n\n            The profile is used for routing on both the first and last mile.\n\n            Elevation cost profiles are currently used by following street modes:\n            - `BIKE`\n          $ref: \"#/components/schemas/ElevationCosts\"\n          default: NONE\n        transitModes:\n          description: |\n            Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n            Allowed modes for the transit part. If empty, no transit connections will be computed.\n            For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n          type: array\n          items:\n            $ref: \"#/components/schemas/Mode\"\n          default:\n            - TRANSIT\n          explode: false\n        preTransitModes:\n          description: |\n            Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n\n            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`.\n          type: array\n          items:\n            $ref: \"#/components/schemas/Mode\"\n          default:\n            - WALK\n          explode: false\n        postTransitModes:\n          description: |\n            Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n\n            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`.\n          type: array\n          items:\n            $ref: \"#/components/schemas/Mode\"\n          default:\n            - WALK\n          explode: false\n        directMode:\n          description: |\n            Default is `WALK` which will compute walking routes as direct connections.\n\n            Mode used for direction connections from start to destination without using transit.\n\n            Currently supported non-transit modes: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`\n          $ref: \"#/components/schemas/Mode\"\n          default: WALK\n        maxPreTransitTime:\n          description: |\n            Optional. Default is 15min which is `900`.\n            Maximum time in seconds for the first street leg.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          type: integer\n          default: 900\n          minimum: 0\n        maxPostTransitTime:\n          description: |\n            Optional. Default is 15min which is `900`.\n            Maximum time in seconds for the last street leg.\n            Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n          type: integer\n          default: 900\n          minimum: 0\n        maxDirectTime:\n          description: |\n            Optional. Default is 30min which is `1800`.\n            Maximum time in seconds for direct connections.\n\n            If a value smaller than either `maxPreTransitTime` or\n            `maxPostTransitTime` is used, their maximum is set instead.\n            Is limited by server config variable `street_routing_max_direct_seconds`.\n          type: integer\n          default: 1800\n          minimum: 0\n        withDistance:\n          description: |\n            If true, the response includes the distance in meters\n            for each path. This requires path reconstruction and\n            may be slower than duration-only queries.\n\n            `withDistance` is currently limited to street routing.\n          type: boolean\n          default: false\n        requireBikeTransport:\n          description: |\n            Optional. Default is `false`.\n\n            If set to `true`, all used transit trips are required to allow bike carriage.\n          type: boolean\n          default: false\n        requireCarTransport:\n          description: |\n            Optional. Default is `false`.\n\n            If set to `true`, all used transit trips are required to allow car carriage.\n          type: boolean\n          default: false\n\n    ServerConfig:\n      Description: server configuration\n      type: object\n      required:\n        - motisVersion\n        - hasElevation\n        - hasRoutedTransfers\n        - hasStreetRouting\n        - maxOneToManySize\n        - maxOneToAllTravelTimeLimit\n        - maxPrePostTransitTimeLimit\n        - maxDirectTimeLimit\n        - shapesDebugEnabled\n      properties:\n        motisVersion:\n          description: the version of this MOTIS server\n          type: string\n        hasElevation:\n          description: true if elevation is loaded\n          type: boolean\n        hasRoutedTransfers:\n          description: true if routed transfers available\n          type: boolean\n        hasStreetRouting:\n          description: true if street routing is available\n          type: boolean\n        maxOneToManySize:\n          description: |\n            limit for the number of `many` locations for one-to-many requests\n          type: number\n        maxOneToAllTravelTimeLimit:\n          description: limit for maxTravelTime API param in minutes\n          type: number\n        maxPrePostTransitTimeLimit:\n          description: limit for maxPrePostTransitTime API param in seconds\n          type: number\n        maxDirectTimeLimit:\n          description: limit for maxDirectTime API param in seconds\n          type: number\n        shapesDebugEnabled:\n          description: true if experimental route shapes debug download API is enabled\n          type: boolean\n    Error:\n      type: object\n      required:\n        - error\n      properties:\n        error:\n          type: string\n          description: error message\n\n    RouteSegment:\n      description: Route segment between two stops to show a route on a map\n      type: object\n      required:\n        - from\n        - to\n        - polyline\n      properties:\n        from:\n          type: integer\n          description: Index into the top-level route stops array\n        to:\n          type: integer\n          description: Index into the top-level route stops array\n        polyline:\n          type: integer\n          description: Index into the top-level route polylines array\n\n    RoutePolyline:\n      description: Shared polyline used by one or more route segments\n      type: object\n      required:\n        - polyline\n        - colors\n        - routeIndexes\n      properties:\n        polyline:\n          $ref: '#/components/schemas/EncodedPolyline'\n        colors:\n          type: array\n          description: Unique route colors of routes containing this segment\n          items:\n            type: string\n        routeIndexes:\n          type: array\n          description: Indexes into the top-level routes array for routes containing this segment\n          items:\n            type: integer\n\n    RouteColor:\n      type: object\n      required:\n        - color\n        - textColor\n      properties:\n        color:\n          type: string\n        textColor:\n          type: string\n\n    RoutePathSource:\n      type: string\n      enum:\n        - NONE\n        - TIMETABLE\n        - ROUTED\n\n    TransitRouteInfo:\n      type: object\n      required:\n        - id\n        - shortName\n        - longName\n      properties:\n        id:\n          type: string\n        shortName:\n          type: string\n        longName:\n          type: string\n        color:\n          type: string\n        textColor:\n          type: string\n\n    RouteInfo:\n      type: object\n      required:\n        - mode\n        - transitRoutes\n        - numStops\n        - routeIdx\n        - pathSource\n        - segments\n      properties:\n        mode:\n          $ref: '#/components/schemas/Mode'\n          description: Transport mode for this route\n        transitRoutes:\n          type: array\n          items:\n            $ref: '#/components/schemas/TransitRouteInfo'\n        numStops:\n          type: integer\n          description: Number of stops along this route\n        routeIdx:\n          type: integer\n          description: Internal route index for debugging purposes\n        pathSource:\n          $ref: '#/components/schemas/RoutePathSource'\n        segments:\n          type: array\n          items:\n            $ref: '#/components/schemas/RouteSegment'\n"
  },
  {
    "path": "src/adr_extend_tt.cc",
    "content": "#include \"osr/geojson.h\"\n\n#include \"motis/adr_extend_tt.h\"\n\n#include \"nigiri/special_stations.h\"\n\n#include <string>\n#include <utility>\n#include <vector>\n\n#include \"utl/get_or_create.h\"\n#include \"utl/parallel_for.h\"\n#include \"utl/timer.h\"\n#include \"utl/to_vec.h\"\n\n#include \"nigiri/timetable.h\"\n#include \"nigiri/translations_view.h\"\n\n#include \"adr/area_database.h\"\n#include \"adr/score.h\"\n#include \"adr/typeahead.h\"\n\n#include \"motis/types.h\"\n\nnamespace a = adr;\nnamespace n = nigiri;\nnamespace json = boost::json;\n\nnamespace motis {\n\nconstexpr auto const kClaszMax =\n    static_cast<std::underlying_type_t<n::clasz>>(n::kNumClasses);\n\ndate::time_zone const* get_tz(n::timetable const& tt,\n                              adr_ext const* ae,\n                              tz_map_t const* tz,\n                              n::location_idx_t const l) {\n  auto const p = tt.locations_.parents_[l];\n  auto const x = p == n::location_idx_t::invalid() ? l : p;\n\n  auto const p_idx =\n      !ae || !tz ? adr_extra_place_idx_t::invalid() : ae->location_place_.at(x);\n  if (p_idx != adr_extra_place_idx_t::invalid()) {\n    return tz->at(p_idx);\n  }\n\n  return nullptr;\n}\n\nvoid normalize(std::string& x) {\n  auto const replace_str = [&](std::string_view search,\n                               std::string_view replace) {\n    auto const pos = x.find(search);\n    if (pos != std::string::npos) {\n      x.replace(pos, search.size(), replace);\n    }\n  };\n\n  replace_str(\"Int.\", \"Hbf\");\n\n  x = adr::normalize(x);\n\n  auto const removals = std::initializer_list<std::string_view>{\n      \"tief\", \"oben\",    \"gleis\", \"platform\", \"gl\",\n      \"gare\", \"bahnhof\", \"bhf\",   \"strasse\",  \"gasse\"};\n  for (auto const r : removals) {\n    auto const pos = x.find(r);\n    if (pos != std::string::npos) {\n      x.erase(pos, r.size());\n    }\n  }\n\n  auto const replacements =\n      std::initializer_list<std::pair<std::string_view, std::string_view>>{\n          {\"flixtrain\", \"hbf\"}, {\"hauptbf\", \"hbf\"},       {\"haupt\", \"hbf\"},\n          {\"centrale\", \"hbf\"},  {\"station\", \"hbf\"},       {\"zob\", \"hbf\"},\n          {\"int\", \"hbf\"},       {\"international\", \"hbf\"}, {\"anleger\", \"fähre\"}};\n  for (auto const& [search, replace] : replacements) {\n    replace_str(search, replace);\n  }\n}\n\nadr::score_t get_diff(std::string str1,\n                      std::string str2,\n                      std::vector<adr::sift_offset>& sift4_offset_arr) {\n  str1 = adr::normalize(str1);\n  str2 = adr::normalize(str2);\n\n  normalize(str1);\n  normalize(str2);\n\n  if (str1.empty() && str2.empty()) {\n    return 0;\n  }\n\n  if (str1.contains(\"hbf\") && str2.contains(\"hbf\")) {\n    return 0;\n  }\n\n  auto a = std::vector<std::string_view>{};\n  auto b = std::vector<std::string_view>{};\n\n  adr::for_each_token(\n      str1,\n      [&](auto&& p_token) mutable {\n        if (!p_token.empty()) {\n          a.emplace_back(p_token);\n        }\n        return utl::continue_t::kContinue;\n      },\n      ' ', '-');\n  adr::for_each_token(\n      str2,\n      [&](auto&& p_token) mutable {\n        if (!p_token.empty()) {\n          b.emplace_back(p_token);\n        }\n        return utl::continue_t::kContinue;\n      },\n      ' ', '-');\n\n  auto covered = std::uint32_t{};\n  auto score = adr::score_t{0U};\n\n  for (auto i = 0U; i != a.size(); ++i) {\n    auto best = std::numeric_limits<adr::score_t>::max();\n    auto best_j = 0U;\n    for (auto j = 0U; j != b.size(); ++j) {\n      if ((covered & (1U << j)) != 0U) {\n        continue;\n      }\n\n      auto const dist = std::min(\n          static_cast<adr::edit_dist_t>(std::max(a[i].size(), b[i].size())),\n          adr::sift4(a[i], b[j], 3,\n                     static_cast<adr::edit_dist_t>(\n                         std::min(a[i].size(), b[j].size()) / 2U + 2U),\n                     sift4_offset_arr));\n      if (dist < best) {\n        best = dist;\n        best_j = j;\n      }\n    }\n\n    covered |= (1U << best_j);\n    score += best;\n  }\n\n  for (auto j = 0U; j != b.size(); ++j) {\n    if ((covered & (1U << j)) == 0U) {\n      score += b[j].size();\n    }\n  }\n\n  return static_cast<float>(score) /\n         static_cast<float>(std::max(str1.length(), str2.length()));\n}\n\nadr_ext adr_extend_tt(nigiri::timetable const& tt,\n                      a::area_database const* area_db,\n                      a::typeahead& t) {\n  if (tt.n_locations() == 0) {\n    return {};\n  }\n\n  auto const timer = utl::scoped_timer{\"guesser candidates\"};\n\n  auto ret = adr_ext{};\n\n  auto area_set_lookup = [&]() {\n    auto x = hash_map<basic_string<a::area_idx_t>, a::area_set_idx_t>{};\n    for (auto const [i, area_set] : utl::enumerate(t.area_sets_)) {\n      x.emplace(area_set.view(), a::area_set_idx_t{i});\n    }\n    return x;\n  }();\n\n  // mapping: location_idx -> place_idx\n  // reverse: place_idx -> location_idx\n  auto place_location = n::vecvec<adr_extra_place_idx_t, n::location_idx_t>{};\n  auto const add_place = [&](n::location_idx_t const l) {\n    auto const i = adr_extra_place_idx_t{place_location.size()};\n    ret.location_place_[l] = i;\n    place_location.emplace_back({l});\n    return i;\n  };\n\n  auto const get_transitive_equivalences = [&](n::location_idx_t const l) {\n    auto q = std::vector<n::location_idx_t>{};\n    auto visited = hash_set<n::location_idx_t>{};\n\n    auto const visit = [&](n::location_idx_t const x) {\n      auto const [_, inserted] = visited.insert(x);\n      if (!inserted) {\n        return;\n      }\n      for (auto const eq : tt.locations_.equivalences_[x]) {\n        if (!visited.contains(eq)) {\n          q.push_back(eq);\n        }\n      }\n    };\n\n    visit(l);\n    while (!q.empty()) {\n      auto const next = q.back();\n      q.resize(q.size() - 1);\n      visit(next);\n    }\n\n    return visited;\n  };\n\n  {\n    ret.location_place_.resize(tt.n_locations(),\n                               adr_extra_place_idx_t::invalid());\n\n    // Map each location + its equivalents with the same name to one\n    // place_idx.\n    auto sift4_dist = std::vector<adr::sift_offset>{};\n    for (auto l = n::location_idx_t{nigiri::kNSpecialStations};\n         l != tt.n_locations(); ++l) {\n      if (ret.location_place_[l] != adr_extra_place_idx_t::invalid() ||\n          tt.locations_.parents_[l] != n::location_idx_t::invalid()) {\n        continue;\n      }\n\n      auto const place_idx = add_place(l);\n\n      auto const name = tt.get_default_translation(tt.locations_.names_[l]);\n      for (auto const eq : get_transitive_equivalences(l)) {\n        if (ret.location_place_[eq] != adr_extra_place_idx_t::invalid() ||\n            tt.locations_.parents_[eq] != n::location_idx_t::invalid()) {\n          continue;\n        }\n\n        if (tt.get_default_translation(tt.locations_.names_[eq]) == name) {\n          ret.location_place_[eq] = place_idx;\n        } else {\n          auto const dist = geo::distance(tt.locations_.coordinates_[l],\n                                          tt.locations_.coordinates_[eq]);\n          auto const eq_name =\n              tt.get_default_translation(tt.locations_.names_[eq]);\n          auto const str_diff =\n              get_diff(std::string{name}, std::string{eq_name}, sift4_dist);\n          auto const cutoff = (500.F - 1750.F * str_diff);\n          auto const good = dist < cutoff;\n\n          if (good) {\n            ret.location_place_[eq] = place_idx;\n\n            auto existing = place_location.back();\n            if (utl::find_if(existing, [&](n::location_idx_t const x) {\n                  return tt.get_default_translation(tt.locations_.names_[x]) ==\n                         tt.get_default_translation(tt.locations_.names_[eq]);\n                }) == end(existing)) {\n              place_location.back().push_back(eq);\n            }\n          }\n        }\n      }\n    }\n\n    // Map all children to root.\n    for (auto l = n::location_idx_t{0U}; l != tt.n_locations(); ++l) {\n      if (tt.locations_.parents_[l] != n::location_idx_t::invalid()) {\n        ret.location_place_[l] =\n            ret.location_place_[tt.locations_.get_root_idx(l)];\n      }\n    }\n  }\n\n  for (auto const [i, place] : utl::enumerate(ret.location_place_)) {\n    if (place == adr_extra_place_idx_t::invalid()) {\n      place = adr_extra_place_idx_t{0U};\n    }\n  }\n\n  for (auto const [i, p] : utl::enumerate(ret.location_place_)) {\n    auto const l = n::location_idx_t{i};\n    if (l >= n::kNSpecialStations && p == adr_extra_place_idx_t::invalid()) {\n      auto const parent =\n          tt.locations_.parents_[l] == n::location_idx_t::invalid()\n              ? n::get_special_station(n::special_station::kEnd)\n              : tt.locations_.parents_[l];\n      auto const grand_parent =\n          tt.locations_.parents_[parent] == n::location_idx_t::invalid()\n              ? n::get_special_station(n::special_station::kEnd)\n              : tt.locations_.parents_[parent];\n\n      utl::log_error(\n          \"adr_extend\", \"invalid place for {} (parent={}, grand_parent={})\",\n          n::loc{tt, l}, n::loc{tt, parent}, n::loc{tt, grand_parent});\n\n      ret.location_place_[l] = adr_extra_place_idx_t{0U};\n    }\n  }\n\n  // For each station without parent:\n  // Compute importance = transport count weighted by clasz.\n  ret.place_importance_.resize(place_location.size());\n  ret.place_clasz_.resize(place_location.size());\n  {\n    auto const event_counts = utl::scoped_timer{\"guesser event_counts\"};\n    for (auto i = n::kNSpecialStations; i < tt.n_locations(); ++i) {\n      auto const l = n::location_idx_t{i};\n\n      auto transport_counts = std::array<unsigned, n::kNumClasses>{};\n      for (auto const& r : tt.location_routes_[l]) {\n        auto const clasz =\n            static_cast<std::underlying_type_t<n::clasz>>(tt.route_clasz_[r]);\n        for (auto const tr : tt.route_transport_ranges_[r]) {\n          transport_counts[clasz] +=\n              tt.bitfields_[tt.transport_traffic_days_[tr]].count();\n        }\n      }\n\n      constexpr auto const prio =\n          std::array<float, kClaszMax>{/* Air */ 300,\n                                       /* HighSpeed */ 300,\n                                       /* LongDistance */ 250,\n                                       /* Coach */ 150,\n                                       /* Night */ 250,\n                                       /* RideSharing */ 5,\n                                       /* Regional */ 100,\n                                       /* Suburban */ 80,\n                                       /* Subway */ 80,\n                                       /* Tram */ 3,\n                                       /* Bus  */ 2,\n                                       /* Ship  */ 10,\n                                       /* CableCar */ 5,\n                                       /* Funicular */ 5,\n                                       /* AerialLift */ 5,\n                                       /* Other  */ 1};\n      auto const root = tt.locations_.get_root_idx(l);\n      auto const place_idx = ret.location_place_[root];\n\n      for (auto const [clasz, t_count] : utl::enumerate(transport_counts)) {\n        ret.place_importance_[place_idx] +=\n            prio[clasz] * static_cast<float>(t_count);\n\n        auto const c = n::clasz{static_cast<std::uint8_t>(clasz)};\n        if (t_count != 0U) {\n          ret.place_clasz_[place_idx] |= n::routing::to_mask(c);\n        }\n      }\n    }\n  }\n\n  // Update counts of meta-stations with the sum of their priorities.\n  // Meta stations have equivalence relations to other stops and are at (0,0)\n  for (auto i = n::kNSpecialStations; i < tt.n_locations(); ++i) {\n    auto const l = n::location_idx_t{i};\n    auto const is_meta =\n        tt.locations_.coordinates_[l] == geo::latlng{} &&\n        tt.locations_.parents_[l] == n::location_idx_t::invalid() &&\n        !tt.locations_.equivalences_[l].empty();\n    if (!is_meta) {\n      continue;\n    }\n\n    auto const place_idx = ret.location_place_[l];\n    for (auto const eq : get_transitive_equivalences(l)) {\n      auto const eq_root = tt.locations_.get_root_idx(eq);\n      auto const eq_place_idx = ret.location_place_[eq_root];\n      ret.place_importance_[place_idx] += ret.place_importance_[eq_place_idx];\n      ret.place_clasz_[place_idx] |= ret.place_clasz_[eq_place_idx];\n    }\n  }\n\n  utl::verify(!ret.place_importance_.empty(), \"no places\");\n\n  // Normalize to interval [0, 1] by dividing by max. importance.\n  {\n    auto const normalize = utl::scoped_timer{\"guesser normalize\"};\n    auto const max_it = std::max_element(begin(ret.place_importance_),\n                                         end(ret.place_importance_));\n    auto const max_importance = std::max(*max_it, 1.F);\n    for (auto& i : ret.place_importance_) {\n      i /= max_importance;\n    }\n  }\n\n  // Add to typeahead.\n  auto const add_string = [&](std::string_view s,\n                              a::place_idx_t const place_idx) {\n    auto const str_idx = a::string_idx_t{t.strings_.size()};\n    t.strings_.emplace_back(s);\n    t.string_to_location_.emplace_back(\n        std::initializer_list<std::uint32_t>{to_idx(place_idx)});\n    t.string_to_type_.emplace_back(\n        std::initializer_list<a::location_type_t>{a::location_type_t::kPlace});\n    return str_idx;\n  };\n  auto areas = basic_string<a::area_idx_t>{};\n  auto no_areas_idx = adr::area_set_idx_t{t.area_sets_.size()};\n  if (area_db == nullptr) {\n    t.area_sets_.emplace_back(areas);\n  }\n\n  for (auto const [prio, locations] :\n       utl::zip(ret.place_importance_, place_location)) {\n    auto const place_idx = a::place_idx_t{t.place_names_.size()};\n\n    auto names = std::vector<std::pair<a::string_idx_t, a::language_idx_t>>{};\n    auto const add_names = [&](n::location_idx_t const loc) {\n      for (auto const [lang, text] :\n           n::get_translation_view(tt, tt.locations_.names_[loc])) {\n        names.emplace_back(add_string(text, place_idx),\n                           t.get_or_create_lang_idx(tt.languages_.get(lang)));\n      }\n    };\n\n    for (auto const l : locations) {\n      add_names(l);\n      for (auto const& c : tt.locations_.children_[l]) {\n        if (tt.locations_.types_[c] == nigiri::location_type::kStation &&\n            tt.get_default_translation(tt.locations_.names_[c]) !=\n                tt.get_default_translation(tt.locations_.names_[l])) {\n          add_names(c);\n        }\n      }\n\n      auto const is_null_island = [](geo::latlng const& pos) {\n        return pos.lat() < 3.0 && pos.lng() < 3.0;\n      };\n      auto pos = tt.locations_.coordinates_[l];\n      if (is_null_island(pos)) {\n        for (auto const c : tt.locations_.children_[l]) {\n          if (!is_null_island(tt.locations_.coordinates_[c])) {\n            pos = tt.locations_.coordinates_[c];\n            break;\n          }\n        }\n      }\n    }\n\n    auto const pos =\n        a::coordinates::from_latlng(tt.locations_.coordinates_[locations[0]]);\n\n    fmt::println(std::clog, \"names: {}, stops={}, prio={}\",\n                 names | std::views::transform([&](auto&& n) {\n                   return t.strings_[n.first].view();\n                 }),\n                 locations | std::views::transform(\n                                 [&](auto&& l) { return n::loc{tt, l}; }),\n                 prio);\n\n    t.place_type_.emplace_back(a::amenity_category::kExtra);\n    t.place_names_.emplace_back(\n        names | std::views::transform([](auto&& n) { return n.first; }));\n    t.place_name_lang_.emplace_back(\n        names | std::views::transform([](auto&& n) { return n.second; }));\n    t.place_coordinates_.emplace_back(pos);\n    t.place_osm_ids_.emplace_back(\n        locations | std::views::transform([&](auto&& l) { return to_idx(l); }));\n    t.place_population_.emplace_back(static_cast<std::uint16_t>(\n        (prio * 1'000'000) / a::population::kCompressionFactor));\n    t.place_is_way_.resize(t.place_is_way_.size() + 1U);\n\n    if (area_db == nullptr) {\n      t.place_areas_.emplace_back(no_areas_idx);\n    } else {\n      area_db->lookup(t, a::coordinates::from_latlng(pos), areas);\n      t.place_areas_.emplace_back(\n          utl::get_or_create(area_set_lookup, areas, [&]() {\n            auto const set_idx = a::area_set_idx_t{t.area_sets_.size()};\n            t.area_sets_.emplace_back(areas);\n            return set_idx;\n          }));\n    }\n  }\n\n  t.build_ngram_index();\n\n  return ret;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/analyze_shapes.cc",
    "content": "#include \"motis/analyze_shapes.h\"\n\n#include \"utl/verify.h\"\n\n#include \"fmt/base.h\"\n#include \"fmt/ranges.h\"\n\n#include \"nigiri/shapes_storage.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/tag_lookup.h\"\n\nnamespace motis {\n\nbool analyze_shape(nigiri::shapes_storage const& shapes,\n                   std::string const& trip_id,\n                   nigiri::trip_idx_t const& trip_idx) {\n  auto const offset_idx = shapes.trip_offset_indices_[trip_idx].second;\n  if (offset_idx == nigiri::shape_offset_idx_t::invalid()) {\n    fmt::println(\"No shape offsets for trip-id '{}'\\n\", trip_id);\n    return false;\n  }\n\n  auto const offsets = shapes.offsets_[offset_idx];\n  if (offsets.empty()) {\n    fmt::println(\"Empty shape for trip-id '{}'\\n\", trip_id);\n    return false;\n  }\n\n  fmt::println(\"Offsets for trip-id '{}':\\n{}\\n\", trip_id, offsets);\n\n  return true;\n}\n\nbool analyze_shapes(data const& d, std::vector<std::string> const& trip_ids) {\n  utl::verify(d.tt_ != nullptr, \"Missing timetable\");\n  utl::verify(d.tags_ != nullptr, \"Missing tags\");\n  utl::verify(d.shapes_ != nullptr, \"Missing shapes\");\n\n  auto success = true;\n  for (auto const& trip_id : trip_ids) {\n    auto const [run, trip_idx] = d.tags_->get_trip(*d.tt_, nullptr, trip_id);\n    if (!run.valid()) {\n      success = false;\n      fmt::println(\"Failed to find trip-id '{}'\\n\", trip_id);\n      continue;\n    }\n    success &= analyze_shape(*d.shapes_, trip_id, trip_idx);\n  }\n  return success;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/clog_redirect.cc",
    "content": "#include \"motis/clog_redirect.h\"\n\n#include <iostream>\n#include <mutex>\n\nnamespace motis {\n\nnamespace {\n\nstruct synchronized_streambuf : std::streambuf {\n  synchronized_streambuf(std::streambuf* wrapped, std::mutex& mutex)\n      : wrapped_{wrapped}, mutex_{mutex} {}\n\n  int_type overflow(int_type ch) override {\n    auto const lock = std::lock_guard{mutex_};\n    if (traits_type::eq_int_type(ch, traits_type::eof())) {\n      return wrapped_->pubsync() == 0 ? traits_type::not_eof(ch)\n                                      : traits_type::eof();\n    }\n    return wrapped_->sputc(traits_type::to_char_type(ch));\n  }\n\n  std::streamsize xsputn(char const* s, std::streamsize count) override {\n    auto const lock = std::lock_guard{mutex_};\n    return wrapped_->sputn(s, count);\n  }\n\n  int sync() override {\n    auto const lock = std::lock_guard{mutex_};\n    return wrapped_->pubsync();\n  }\n\nprivate:\n  std::streambuf* wrapped_;\n  std::mutex& mutex_;\n};\n\n}  // namespace\n\nclog_redirect::clog_redirect(char const* log_file_path) {\n  if (!enabled_) {\n    return;\n  }\n\n  sink_.exceptions(std::ios_base::badbit | std::ios_base::failbit);\n  sink_.open(log_file_path, std::ios_base::app);\n  sink_buf_ = std::make_unique<synchronized_streambuf>(sink_.rdbuf(), mutex_);\n\n  backup_clog_ = std::clog.rdbuf();\n  std::clog.rdbuf(sink_buf_.get());\n  active_ = true;\n}\n\nclog_redirect::~clog_redirect() {\n  if (!active_) {\n    return;\n  }\n\n  auto const lock = std::lock_guard{mutex_};\n  std::clog.rdbuf(backup_clog_);\n}\n\nvoid clog_redirect::set_enabled(bool const enabled) { enabled_ = enabled; }\n\n// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)\nbool clog_redirect::enabled_ = true;\n\n}  // namespace motis"
  },
  {
    "path": "src/compute_footpaths.cc",
    "content": "#include \"motis/compute_footpaths.h\"\n\n#include \"nigiri/loader/build_lb_graph.h\"\n\n#include \"cista/mmap.h\"\n#include \"cista/serialization.h\"\n\n#include \"utl/concat.h\"\n#include \"utl/erase_if.h\"\n#include \"utl/parallel_for.h\"\n#include \"utl/sorted_diff.h\"\n\n#include \"osr/routing/profiles/foot.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/util/infinite.h\"\n#include \"osr/util/reverse.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/get_loc.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/max_distance.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/point_rtree.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nelevator_footpath_map_t compute_footpaths(\n    osr::ways const& w,\n    osr::lookup const& lookup,\n    osr::platforms const& pl,\n    nigiri::timetable& tt,\n    osr::elevation_storage const* elevations,\n    bool const update_coordinates,\n    std::vector<routed_transfers_settings> const& settings) {\n  fmt::println(std::clog, \"creating matches\");\n  auto const matches = get_matches(tt, pl, w);\n\n  fmt::println(std::clog, \"  -> creating r-tree\");\n  auto const loc_rtree = [&]() {\n    auto t = point_rtree<n::location_idx_t>{};\n    for (auto i = n::location_idx_t{0U}; i != tt.n_locations(); ++i) {\n      if (update_coordinates && matches[i] != osr::platform_idx_t::invalid()) {\n        auto const center = get_platform_center(pl, w, matches[i]);\n        if (center.has_value() &&\n            geo::distance(*center, tt.locations_.coordinates_[i]) <\n                kMaxAdjust) {\n          tt.locations_.coordinates_[i] = *center;\n        }\n      }\n      t.add(tt.locations_.coordinates_[i], i);\n    }\n    return t;\n  }();\n\n  auto const pt = utl::get_active_progress_tracker();\n  pt->in_high(2U * tt.n_locations() * settings.size());\n\n  auto elevator_in_paths_mutex = std::mutex{};\n  auto elevator_in_paths = elevator_footpath_map_t{};\n  auto const add_if_elevator = [&](osr::node_idx_t const n,\n                                   n::location_idx_t const a,\n                                   n::location_idx_t const b) {\n    if (n != osr::node_idx_t::invalid() &&\n        w.r_->node_properties_[n].is_elevator()) {\n      auto l = std::unique_lock{elevator_in_paths_mutex};\n      elevator_in_paths[n].emplace(a, b);\n    }\n  };\n\n  struct state {\n    std::vector<n::footpath> sorted_tt_fps_;\n    std::vector<n::footpath> missing_;\n    std::vector<n::location_idx_t> neighbors_;\n    std::vector<osr::location> neighbors_loc_;\n    std::vector<osr::match_t> neighbor_candidates_;\n  };\n\n  auto n_done = 0U;\n  auto candidates = vector_map<n::location_idx_t, osr::match_t>{};\n  auto transfers = n::vector_map<n::location_idx_t, std::vector<n::footpath>>(\n      tt.n_locations());\n  for (auto const& mode : settings) {\n    candidates.clear();\n    candidates.resize(tt.n_locations());\n    for (auto& fps : transfers) {\n      fps.clear();\n    }\n\n    auto const is_candidate = [&](n::location_idx_t const l) {\n      return !mode.is_candidate_ || mode.is_candidate_(l);\n    };\n\n    {\n      auto const timer = utl::scoped_timer{\n          fmt::format(\"matching timetable locations for profile={}\",\n                      to_str(mode.profile_))};\n\n      utl::parallel_for_run(tt.n_locations(), [&](std::size_t const x) {\n        pt->update_monotonic(n_done + x);\n\n        auto const l =\n            n::location_idx_t{static_cast<n::location_idx_t::value_t>(x)};\n        if (!is_candidate(l)) {\n          return;\n        }\n        candidates[l] = lookup.match(\n            to_profile_parameters(mode.profile_, {}),\n            get_loc(tt, w, pl, matches, l), false, osr::direction::kForward,\n            mode.max_matching_distance_, nullptr, mode.profile_);\n      });\n\n      n_done += tt.n_locations();\n    }\n\n    utl::parallel_for_run_threadlocal<state>(\n        tt.n_locations(), [&](state& s, auto const i) {\n          cista::for_each_field(s, [](auto& f) { f.clear(); });\n\n          auto const l = n::location_idx_t{i};\n          if (!is_candidate(l)) {\n            pt->update_monotonic(n_done + i);\n            return;\n          }\n\n          loc_rtree.in_radius(\n              tt.locations_.coordinates_[l],\n              get_max_distance(mode.profile_, mode.max_duration_),\n              [&](n::location_idx_t const x) {\n                if (x != l && is_candidate(x)) {\n                  s.neighbors_.emplace_back(x);\n                }\n              });\n\n          auto const results = osr::route(\n              to_profile_parameters(mode.profile_, {}), w, lookup,\n              mode.profile_, get_loc(tt, w, pl, matches, l),\n              utl::transform_to(s.neighbors_, s.neighbors_loc_,\n                                [&](n::location_idx_t const x) {\n                                  return get_loc(tt, w, pl, matches, x);\n                                }),\n              candidates[l],\n              utl::transform_to(\n                  s.neighbors_, s.neighbor_candidates_,\n                  [&](n::location_idx_t const x) { return candidates[x]; }),\n              static_cast<osr::cost_t>(mode.max_duration_.count()),\n              osr::direction::kForward, nullptr, nullptr, elevations,\n              [](osr::path const& p) { return p.uses_elevator_; });\n\n          for (auto const [n, r] : utl::zip(s.neighbors_, results)) {\n            if (!r.has_value()) {\n              continue;\n            }\n\n            auto const duration = n::duration_t{\n                static_cast<unsigned>(std::ceil(r->cost_ / 60.0))};\n            transfers[l].emplace_back(n::footpath{n, duration});\n\n            if (mode.profile_ == osr::search_profile::kWheelchair) {\n              for (auto const& seg : r->segments_) {\n                add_if_elevator(seg.from_, l, n);\n                add_if_elevator(seg.from_, n, l);\n              }\n            }\n          }\n\n          if (mode.extend_missing_) {\n            auto const& tt_fps = tt.locations_.footpaths_out_[0].at(l);\n            s.sorted_tt_fps_.resize(tt_fps.size());\n            std::copy(begin(tt_fps), end(tt_fps), begin(s.sorted_tt_fps_));\n            utl::sort(s.sorted_tt_fps_);\n            utl::sort(transfers[l]);\n\n            utl::sorted_diff(\n                s.sorted_tt_fps_, transfers[l],\n                [](auto&& a, auto&& b) { return a.target() < b.target(); },\n                [](auto&& a, auto&& b) { return a.target() == b.target(); },\n                utl::overloaded{\n                    [](n::footpath, n::footpath) { assert(false); },\n                    [&](utl::op const op, n::footpath const x) {\n                      if (op == utl::op::kDel) {\n                        auto const dist = geo::distance(\n                            tt.locations_.coordinates_[l],\n                            tt.locations_.coordinates_[x.target()]);\n                        if (dist < 100.0) {\n                          auto const duration = n::duration_t{\n                              static_cast<int>(std::ceil((dist / 0.7) / 60.0))};\n                          s.missing_.emplace_back(x.target(), duration);\n                        }\n                      }\n                    }});\n\n            utl::concat(transfers[l], s.missing_);\n          }\n\n          utl::erase_if(transfers[l], [&](n::footpath fp) {\n            return fp.duration() > mode.max_duration_;\n          });\n          utl::sort(transfers[l]);\n\n          pt->update_monotonic(n_done + i);\n        });\n\n    auto transfers_in =\n        n::vector_map<n::location_idx_t, std::vector<n::footpath>>{};\n    transfers_in.resize(tt.n_locations());\n    for (auto const [i, out] : utl::enumerate(transfers)) {\n      auto const l = n::location_idx_t{i};\n      for (auto const fp : out) {\n        assert(fp.target() < tt.n_locations());\n        transfers_in[fp.target()].push_back(n::footpath{l, fp.duration()});\n      }\n    }\n    for (auto const& x : transfers) {\n      tt.locations_.footpaths_out_[mode.profile_idx_].emplace_back(x);\n    }\n    for (auto const& x : transfers_in) {\n      tt.locations_.footpaths_in_[mode.profile_idx_].emplace_back(x);\n    }\n\n    n::loader::build_lb_graph<n::direction::kForward>(tt, mode.profile_idx_);\n    n::loader::build_lb_graph<n::direction::kBackward>(tt, mode.profile_idx_);\n\n    n_done += tt.n_locations();\n  }\n\n  return elevator_in_paths;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/config.cc",
    "content": "#include \"motis/config.h\"\n\n#include <iostream>\n\n#include \"boost/url.hpp\"\n\n#include \"fmt/std.h\"\n\n#include \"utl/erase.h\"\n#include \"utl/overloaded.h\"\n#include \"utl/read_file.h\"\n#include \"utl/verify.h\"\n\n#include \"nigiri/clasz.h\"\n#include \"nigiri/routing/limits.h\"\n\n#include \"rfl.hpp\"\n#include \"rfl/yaml.hpp\"\n\nnamespace fs = std::filesystem;\n\nnamespace motis {\n\ntemplate <rfl::internal::StringLiteral Name>\nconsteval auto drop_last() {\n  return []<size_t... Is>(std::index_sequence<Is...>) {\n    return rfl::internal::StringLiteral<Name.arr_.size() - 1>(Name.arr_[Is]...);\n  }(std::make_index_sequence<Name.arr_.size() - 2>{});\n}\n\nstruct drop_trailing {\npublic:\n  template <typename StructType>\n  static auto process(auto&& named_tuple) {\n    auto const handle_one = []<typename FieldType>(FieldType&& f) {\n      if constexpr (FieldType::name() != \"xml_content\" &&\n                    !rfl::internal::is_rename_v<typename FieldType::Type>) {\n        return handle_one_field(std::move(f));\n      } else {\n        return std::move(f);\n      }\n    };\n    return named_tuple.transform(handle_one);\n  }\n\nprivate:\n  template <typename FieldType>\n  static auto handle_one_field(FieldType&& _f) {\n    using NewFieldType =\n        rfl::Field<drop_last<FieldType::name_>(), typename FieldType::Type>;\n    return NewFieldType(_f.value());\n  }\n};\n\nstd::ostream& operator<<(std::ostream& out, config const& c) {\n  return out << rfl::yaml::write<drop_trailing>(c);\n}\n\nconfig config::read_simple(std::vector<std::string> const& args) {\n  auto c = config{};\n  for (auto const& arg : args) {\n    auto const p = fs::path{arg};\n    utl::verify(fs::exists(p), \"path {} does not exist\", p);\n    if (fs::is_regular_file(p) && p.generic_string().ends_with(\"osm.pbf\")) {\n      c.osm_ = p;\n      c.street_routing_ = true;\n      c.geocoding_ = true;\n      c.reverse_geocoding_ = true;\n      c.tiles_ = {config::tiles{.profile_ = \"tiles-profiles/full.lua\"}};\n    } else {\n      if (!c.timetable_.has_value()) {\n        c.timetable_ = {timetable{.railviz_ = true}};\n      }\n\n      auto tag = p.stem().generic_string();\n      utl::erase(tag, '_');\n      utl::erase(tag, '.');\n      c.timetable_->datasets_.emplace(\n          tag, timetable::dataset{.path_ = p.generic_string()});\n    }\n  }\n  return c;\n}\n\nconfig config::read(std::filesystem::path const& p) {\n  auto const file_content = utl::read_file(p.generic_string().c_str());\n  utl::verify(file_content.has_value(), \"could not read config file at {}\", p);\n  return read(*file_content);\n}\n\nconfig config::read(std::string const& s) {\n  auto c =\n      rfl::yaml::read<config, drop_trailing, rfl::DefaultIfMissing>(s).value();\n  if (!c.limits_.has_value()) {\n    c.limits_.emplace(limits{});\n  }\n  c.verify();\n  return c;\n}\n\nvoid config::verify() const {\n  auto const street_routing = use_street_routing();\n\n  utl::verify(!tiles_ || osm_, \"feature TILES requires OpenStreetMap data\");\n  utl::verify(!street_routing || osm_,\n              \"feature STREET_ROUTING requires OpenStreetMap data\");\n  utl::verify(!timetable_ || !timetable_->datasets_.empty(),\n              \"feature TIMETABLE requires timetable data\");\n  utl::verify(\n      !osr_footpath_ || (street_routing && timetable_),\n      \"feature OSR_FOOTPATH requires features STREET_ROUTING and TIMETABLE\");\n  utl::verify(!has_elevators() || (street_routing && timetable_),\n              \"feature ELEVATORS requires STREET_ROUTING and TIMETABLE\");\n  utl::verify(!has_gbfs_feeds() || street_routing,\n              \"feature GBFS requires feature STREET_ROUTING\");\n  utl::verify(!has_prima() || (street_routing && timetable_),\n              \"feature ODM requires feature STREET_ROUTING\");\n  utl::verify(!has_elevators() || osr_footpath_,\n              \"feature ELEVATORS requires feature OSR_FOOTPATHS\");\n  utl::verify(limits_.value().plan_max_search_window_minutes_ <=\n                  nigiri::routing::kMaxSearchIntervalSize.count(),\n              \"plan_max_search_window_minutes limit cannot be above {}\",\n              nigiri::routing::kMaxSearchIntervalSize.count());\n  utl::verify(limits_.value().geocode_max_suggestions_ >= 1U,\n              \"geocode_max_suggestions must be >= 1\");\n  utl::verify(limits_.value().reverse_geocode_max_results_ >= 1U,\n              \"reverse_geocode_max_results must be >= 1\");\n\n  if (timetable_) {\n    utl::verify(!timetable_->route_shapes_.has_value() ||\n                    (timetable_->with_shapes_ && street_routing),\n                \"feature ROUTE_SHAPES requires SHAPES and STREET_ROUTING\");\n\n    for (auto const& [id, d] : timetable_->datasets_) {\n      utl::verify(!id.contains(\"_\"), \"dataset identifier may not contain '_'\");\n      if (d.rt_.has_value()) {\n        for (auto const& rt : *d.rt_) {\n          try {\n            boost::urls::url{rt.url_};\n          } catch (std::exception const& e) {\n            throw utl::fail(\"{} is not a valid url: {}\", rt.url_, e.what());\n          }\n          utl::verify(rt.protocol_ != timetable::dataset::rt::protocol::auser ||\n                          timetable_->incremental_rt_update_,\n                      \"VDV AUS requires incremental RT update scheme\");\n        }\n      }\n    }\n  }\n}\n\nvoid config::verify_input_files_exist() const {\n  utl::verify(!osm_ || fs::is_regular_file(*osm_),\n              \"OpenStreetMap file does not exist: {}\",\n              osm_.value_or(fs::path{}));\n\n  utl::verify(!tiles_ || fs::is_regular_file(tiles_->profile_),\n              \"tiles profile {} does not exist\",\n              tiles_.value_or(tiles{}).profile_);\n\n  utl::verify(!tiles_ || !tiles_->coastline_ ||\n                  fs::is_regular_file(*tiles_->coastline_),\n              \"coastline file {} does not exist\",\n              tiles_.value_or(tiles{}).coastline_.value_or(\"\"));\n\n  if (timetable_) {\n    for (auto const& [tag, d] : timetable_->datasets_) {\n      utl::verify(d.path_.starts_with(\"\\n#\") || fs::is_directory(d.path_) ||\n                      fs::is_regular_file(d.path_),\n                  \"timetable dataset {} does not exist: {}\", tag, d.path_);\n\n      utl::verify(!d.script_.has_value() ||\n                      d.script_->starts_with(\"\\nfunction\") ||\n                      fs::is_regular_file(*d.script_),\n                  \"user script for {} not found at path: \\\"{}\\\"\", tag,\n                  d.script_.value_or(\"\"));\n\n      if (d.clasz_bikes_allowed_.has_value()) {\n        for (auto const& c : *d.clasz_bikes_allowed_) {\n          nigiri::to_clasz(c.first);\n        }\n      }\n    }\n\n    if (timetable_->route_shapes_) {\n      if (timetable_->route_shapes_->clasz_) {\n        for (auto const& c : *timetable_->route_shapes_->clasz_) {\n          nigiri::to_clasz(c.first);\n        }\n      }\n    }\n  }\n}\n\nbool config::requires_rt_timetable_updates() const {\n  return timetable_.has_value() &&\n         ((has_elevators() && get_elevators()->url_.has_value()) ||\n          utl::any_of(timetable_->datasets_, [](auto&& d) {\n            return d.second.rt_.has_value() && !d.second.rt_->empty();\n          }));\n}\n\nbool config::shapes_debug_api_enabled() const {\n  return timetable_.has_value() && timetable_->route_shapes_.has_value() &&\n         timetable_->route_shapes_->debug_api_;\n}\n\nbool config::has_gbfs_feeds() const {\n  return gbfs_.has_value() && !gbfs_->feeds_.empty();\n}\n\nbool config::has_prima() const { return prima_.has_value(); }\n\nunsigned config::n_threads() const {\n  return server_\n      .and_then([](config::server const& s) {\n        return s.n_threads_ == 0U ? std::nullopt : std::optional{s.n_threads_};\n      })\n      .value_or(std::thread::hardware_concurrency());\n}\n\nstd::optional<config::elevators> const& config::get_elevators() const {\n  utl::verify(has_elevators(),\n              \"config::get_elevators() requires config::has_elevators()\");\n  return std::get<std::optional<elevators>>(elevators_);\n}\n\nbool config::has_elevators() const {\n  return std::visit(\n      utl::overloaded{\n          [](std::optional<elevators> const& x) { return x.has_value(); },\n          [](bool const x) {\n            utl::verify(!x, \"elevators=true is not supported\");\n            return x;\n          }},\n      elevators_);\n}\n\nstd::optional<config::street_routing> config::get_street_routing() const {\n  return std::visit(\n      utl::overloaded{\n          [](std::optional<config::street_routing> const& x) { return x; },\n          [](bool const street_routing) {\n            return street_routing ? std::optional{config::street_routing{}}\n                                  : std::nullopt;\n          }},\n      street_routing_);\n}\n\nbool config::use_street_routing() const {\n  return std::visit(\n      utl::overloaded{\n          [](std::optional<street_routing> const& o) { return o.has_value(); },\n          [](bool const b) { return b; },\n      },\n      street_routing_);\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/data.cc",
    "content": "#include \"motis/data.h\"\n\n#include <filesystem>\n#include <future>\n\n#include \"cista/io.h\"\n\n#include \"utl/get_or_create.h\"\n#include \"utl/read_file.h\"\n#include \"utl/verify.h\"\n\n#include \"adr/adr.h\"\n#include \"adr/cache.h\"\n#include \"adr/formatter.h\"\n#include \"adr/reverse.h\"\n#include \"adr/typeahead.h\"\n\n#include \"osr/elevation_storage.h\"\n#include \"osr/lookup.h\"\n#include \"osr/platforms.h\"\n#include \"osr/ways.h\"\n\n#include \"nigiri/routing/tb/tb_data.h\"\n#include \"nigiri/rt/create_rt_timetable.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/shapes_storage.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/config.h\"\n#include \"motis/constants.h\"\n#include \"motis/elevators/update_elevators.h\"\n#include \"motis/endpoints/initial.h\"\n#include \"motis/flex/flex_areas.h\"\n#include \"motis/hashes.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/metrics_registry.h\"\n#include \"motis/odm/bounds.h\"\n#include \"motis/point_rtree.h\"\n#include \"motis/railviz.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/tiles_data.h\"\n#include \"motis/tt_location_rtree.h\"\n\nnamespace fs = std::filesystem;\nnamespace n = nigiri;\n\nnamespace motis {\n\nrt::rt() = default;\n\nrt::rt(ptr<nigiri::rt_timetable>&& rtt,\n       ptr<elevators>&& e,\n       ptr<railviz_rt_index>&& railviz)\n    : rtt_{std::move(rtt)}, railviz_rt_{std::move(railviz)}, e_{std::move(e)} {}\n\nrt::~rt() = default;\n\nstd::ostream& operator<<(std::ostream& out, data const& d) {\n  return out << \"\\nt=\" << d.t_.get() << \"\\nr=\" << d.r_ << \"\\ntc=\" << d.tc_\n             << \"\\nw=\" << d.w_ << \"\\npl=\" << d.pl_ << \"\\nl=\" << d.l_\n             << \"\\ntt=\" << d.tt_.get()\n             << \"\\nlocation_rtee=\" << d.location_rtree_\n             << \"\\nelevator_nodes=\" << d.elevator_nodes_\n             << \"\\nmatches=\" << d.matches_ << \"\\nrt=\" << d.rt_ << \"\\n\";\n}\n\ndata::data(std::filesystem::path p)\n    : path_{std::move(p)},\n      config_{config::read(path_ / \"config.yml\")},\n      metrics_{std::make_unique<metrics_registry>()} {}\n\ndata::data(std::filesystem::path p, config const& c)\n    : path_{std::move(p)},\n      config_{c},\n      metrics_{std::make_unique<metrics_registry>()} {\n  auto const verify_version = [&](bool cond, char const* name, auto&& ver) {\n    if (!cond) {\n      return;\n    }\n    auto const [key, expected_ver] = ver;\n    auto const h = read_hashes(path_, name);\n    auto const existing_ver_it = h.find(key);\n\n    utl::verify(existing_ver_it != end(h),\n                \"{}: no existing version found [key={}], please re-run import; \"\n                \"hashes: {}\",\n                name, key, to_str(h));\n    utl::verify(existing_ver_it->second == expected_ver,\n                \"{}: binary version mismatch [existing={} vs expected={}], \"\n                \"please re-run import; hashes: {}\",\n                name, existing_ver_it->second, expected_ver, to_str(h));\n  };\n\n  verify_version(c.timetable_.has_value(), \"tt\", n_version());\n  verify_version(c.geocoding_ || c.reverse_geocoding_, \"adr\", adr_version());\n  verify_version(c.use_street_routing(), \"osr\", osr_version());\n  verify_version(c.use_street_routing() && c.timetable_, \"matches\",\n                 matches_version());\n  verify_version(c.tiles_.has_value(), \"tiles\", tiles_version());\n  verify_version(c.osr_footpath_, \"osr_footpath\", osr_footpath_version());\n\n  rt_ = std::make_shared<rt>();\n\n  if (c.prima_.has_value()) {\n    if (c.prima_->bounds_.has_value()) {\n      odm_bounds_ = std::make_unique<odm::bounds>(*c.prima_->bounds_);\n    }\n    if (c.prima_->ride_sharing_bounds_.has_value()) {\n      ride_sharing_bounds_ = std::make_unique<odm::ride_sharing_bounds>(\n          *c.prima_->ride_sharing_bounds_);\n    }\n  }\n\n  auto geocoder = std::async(std::launch::async, [&]() {\n    f_ = std::make_unique<adr::formatter>();\n    if (c.geocoding_) {\n      load_geocoder();\n    }\n    if (c.reverse_geocoding_) {\n      load_reverse_geocoder();\n    }\n  });\n\n  auto tt = std::async(std::launch::async, [&]() {\n    if (c.timetable_) {\n      load_tt(config_.osr_footpath_ ? \"tt_ext.bin\" : \"tt.bin\");\n      if (c.timetable_->with_shapes_) {\n        load_shapes();\n      }\n      if (c.timetable_->railviz_) {\n        load_railviz();\n      }\n      if (c.timetable_->tb_) {\n        load_tbd();\n      }\n      for (auto const& [tag, d] : c.timetable_->datasets_) {\n        if (d.rt_ && utl::any_of(*d.rt_, [](auto const& rt) {\n              return rt.protocol_ ==\n                         config::timetable::dataset::rt::protocol::auser ||\n                     rt.protocol_ ==\n                         config::timetable::dataset::rt::protocol::siri ||\n                     rt.protocol_ ==\n                         config::timetable::dataset::rt::protocol::siri_json;\n            })) {\n          load_auser_updater(tag, d);\n        }\n      }\n    }\n  });\n\n  auto street_routing = std::async(std::launch::async, [&]() {\n    if (c.use_street_routing()) {\n      load_osr();\n    }\n  });\n\n  auto fa = std::async(std::launch::async, [&]() {\n    if (c.timetable_ && c.use_street_routing()) {\n      street_routing.wait();\n      tt.wait();\n\n      load_flex_areas();\n    }\n  });\n\n  auto matches = std::async(std::launch::async, [&]() {\n    if (c.use_street_routing() && c.timetable_) {\n      load_matches();\n      load_way_matches();\n    }\n  });\n\n  auto elevators = std::async(std::launch::async, [&]() {\n    if (c.has_elevators()) {\n      street_routing.wait();\n\n      elevator_osm_mapping_ =\n          utl::visit(\n              config_.elevators_,\n              [](std::optional<config::elevators> const& x) { return x; })\n              .and_then([](auto const& x) { return x.osm_mapping_; })\n              .transform([](std::string const& x) {\n                return std::make_unique<elevator_id_osm_mapping_t>(\n                    x.starts_with(\"dhid,diid,osm_kind,osm_id\")\n                        ? parse_elevator_id_osm_mapping(std::string_view{x})\n                        : parse_elevator_id_osm_mapping(fs::path{x}));\n              })\n              .value_or(nullptr);\n      rt_->e_ = std::make_unique<motis::elevators>(\n          *w_, elevator_osm_mapping_.get(), *elevator_nodes_,\n          vector_map<elevator_idx_t, elevator>{});\n\n      if (c.get_elevators()->init_) {\n        tt.wait();\n        auto new_rtt = std::make_unique<n::rt_timetable>(\n            n::rt::create_rt_timetable(*tt_, rt_->rtt_->base_day_));\n        rt_->e_ = update_elevators(\n            c, *this,\n            c.get_elevators()->init_->starts_with(\"\\n\")\n                ? std::string_view{*c.get_elevators()->init_}\n                : cista::mmap{c.get_elevators()->init_->c_str(),\n                              cista::mmap::protection::READ}\n                      .view(),\n            *new_rtt);\n        rt_->rtt_ = std::move(new_rtt);\n      }\n    }\n  });\n\n  auto tiles = std::async(std::launch::async, [&]() {\n    if (c.tiles_) {\n      load_tiles();\n    }\n  });\n\n  auto const throw_if_failed = [](char const* context, auto& future) {\n    try {\n      future.get();\n    } catch (std::exception const& e) {\n      throw utl::fail(\n          \"loading {} failed (if this happens after a fresh import, please \"\n          \"file a bug report): {}\",\n          context, e.what());\n    }\n  };\n\n  geocoder.wait();\n  tt.wait();\n  fa.wait();\n  street_routing.wait();\n  matches.wait();\n  elevators.wait();\n  tiles.wait();\n\n  throw_if_failed(\"geocoder\", geocoder);\n  throw_if_failed(\"tt\", tt);\n  throw_if_failed(\"street_routing\", street_routing);\n  throw_if_failed(\"matches\", matches);\n  throw_if_failed(\"elevators\", elevators);\n  throw_if_failed(\"tiles\", tiles);\n\n  initial_response_ = ep::get_initial_response(*this);\n\n  utl_verify(\n      shapes_ == nullptr || tt_ == nullptr ||\n          (tt_->n_routes() == shapes_->route_bboxes_.size() &&\n           tt_->n_routes() == shapes_->route_segment_bboxes_.size()),\n      \"mismatch: n_routes={}, n_route_bboxes={}, n_route_segment_bboxes={}\",\n      tt_->n_routes(), shapes_->route_bboxes_.size(),\n      shapes_->route_segment_bboxes_.size());\n  utl_verify(matches_ == nullptr || tt_ == nullptr ||\n                 matches_->size() == tt_->n_locations(),\n             \"mismatch: n_matches={}, n_locations={}\", matches_->size(),\n             tt_->n_locations());\n}\n\ndata::~data() = default;\ndata::data(data&&) = default;\ndata& data::operator=(data&&) = default;\n\nvoid data::load_osr() {\n  auto const osr_path = path_ / \"osr\";\n  w_ = std::make_unique<osr::ways>(osr_path, cista::mmap::protection::READ);\n  l_ = std::make_unique<osr::lookup>(*w_, osr_path,\n                                     cista::mmap::protection::READ);\n  if (config_.get_street_routing()->elevation_data_dir_.has_value()) {\n    elevations_ = osr::elevation_storage::try_open(osr_path);\n  }\n  elevator_nodes_ =\n      std::make_unique<hash_set<osr::node_idx_t>>(get_elevator_nodes(*w_));\n  pl_ =\n      std::make_unique<osr::platforms>(osr_path, cista::mmap::protection::READ);\n  pl_->build_rtree(*w_);\n}\n\nvoid data::load_tt(fs::path const& p) {\n  tags_ = tag_lookup::read(path_ / \"tags.bin\");\n  tt_ = n::timetable::read(path_ / p);\n  tt_->resolve();\n  location_rtree_ = std::make_unique<point_rtree<n::location_idx_t>>(\n      create_location_rtree(*tt_));\n  init_rtt();\n}\n\nvoid data::load_flex_areas() {\n  utl::verify(tt_ && w_ && l_, \"flex areas requires tt={}, w={}, l={}\",\n              tt_ != nullptr, w_ != nullptr, l_ != nullptr);\n  flex_areas_ = std::make_unique<flex::flex_areas>(*tt_, *w_, *l_);\n}\n\nvoid data::init_rtt(date::sys_days const d) {\n  rt_->rtt_ =\n      std::make_unique<n::rt_timetable>(n::rt::create_rt_timetable(*tt_, d));\n}\n\nvoid data::load_shapes() {\n  shapes_ = {};\n  shapes_ = std::make_unique<nigiri::shapes_storage>(\n      nigiri::shapes_storage{path_, cista::mmap::protection::READ});\n}\n\nvoid data::load_railviz() {\n  railviz_static_ = std::make_unique<railviz_static_index>(*tt_, shapes_.get());\n  rt_->railviz_rt_ = std::make_unique<railviz_rt_index>(*tt_, *rt_->rtt_);\n}\n\nvoid data::load_tbd() {\n  tbd_ = cista::read<n::routing::tb::tb_data>(path_ / \"tbd.bin\");\n}\n\nvoid data::load_geocoder() {\n  t_ = adr::read(path_ / \"adr\" /\n                 (config_.timetable_.has_value() ? \"t_ext.bin\" : \"t.bin\"));\n  tc_ = std::make_unique<adr::cache>(t_->strings_.size(), 100U);\n\n  if (config_.timetable_.has_value()) {\n    adr_ext_ = cista::read<adr_ext>(path_ / \"adr\" / \"location_extra_place.bin\");\n    tz_ = std::make_unique<\n        vector_map<adr_extra_place_idx_t, date::time_zone const*>>();\n    auto cache = hash_map<std::string, date::time_zone const*>{};\n    for (auto const [type, areas] :\n         utl::zip(t_->place_type_, t_->place_areas_)) {\n      if (type != adr::amenity_category::kExtra) {\n        continue;\n      }\n\n      auto const tz = t_->get_tz(areas);\n      if (tz == adr::timezone_idx_t::invalid()) {\n        tz_->push_back(nullptr);\n      } else {\n        auto const tz_name = t_->timezone_names_[tz].view();\n        tz_->push_back(\n            utl::get_or_create(cache, tz_name, [&]() -> date::time_zone const* {\n              try {\n                return date::locate_zone(tz_name);\n              } catch (...) {\n                return nullptr;\n              }\n            }));\n      }\n    }\n  }\n}\n\nvoid data::load_reverse_geocoder() {\n  r_ = std::make_unique<adr::reverse>(path_ / \"adr\",\n                                      cista::mmap::protection::READ);\n}\n\nvoid data::load_matches() {\n  matches_ = cista::read<platform_matches_t>(path_ / \"matches.bin\");\n}\n\nvoid data::load_way_matches() {\n  if (config_.timetable_.value().preprocess_max_matching_distance_ > 0.0) {\n    way_matches_ = {};\n    way_matches_ = std::make_unique<way_matches_storage>(way_matches_storage{\n        path_, cista::mmap::protection::READ,\n        config_.timetable_.value().preprocess_max_matching_distance_});\n  }\n}\n\nvoid data::load_tiles() {\n  auto const db_size = config_.tiles_.value().db_size_;\n  tiles_ = std::make_unique<tiles_data>(\n      (path_ / \"tiles\" / \"tiles.mdb\").generic_string(), db_size);\n}\n\nvoid data::load_auser_updater(std::string_view tag,\n                              config::timetable::dataset const& d) {\n  if (!auser_) {\n    auser_ = std::make_unique<std::map<std::string, auser>>();\n  }\n\n  auto const convert = [](config::timetable::dataset::rt::protocol const p) {\n    switch (p) {\n      case config::timetable::dataset::rt::protocol::auser:\n        return n::rt::vdv_aus::updater::xml_format::kVdv;\n      case config::timetable::dataset::rt::protocol::siri:\n        return n::rt::vdv_aus::updater::xml_format::kSiri;\n      case config::timetable::dataset::rt::protocol::siri_json:\n        return n::rt::vdv_aus::updater::xml_format::kSiriJson;\n      case config::timetable::dataset::rt::protocol::gtfsrt: std::unreachable();\n    }\n    std::unreachable();\n  };\n\n  for (auto const& rt : *d.rt_) {\n    auser_->try_emplace(rt.url_, *tt_, tags_->get_src(tag),\n                        convert(rt.protocol_));\n  }\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/direct_filter.cc",
    "content": "#include \"motis/direct_filter.h\"\n\n#include \"utl/erase_if.h\"\n#include \"utl/visit.h\"\n\n#include \"nigiri/types.h\"\n\nnamespace motis {\n\nusing namespace std::chrono_literals;\nnamespace n = nigiri;\n\nvoid direct_filter(std::vector<api::Itinerary> const& direct,\n                   std::vector<n::routing::journey>& journeys) {\n  auto const get_direct_duration = [&](auto const transport_mode_id) {\n    auto const m = static_cast<api::ModeEnum>(transport_mode_id);\n    auto const i = utl::find_if(\n        direct, [&](auto const& d) { return d.legs_.front().mode_ == m; });\n    return i != end(direct)\n               ? n::duration_t{std::chrono::round<std::chrono::minutes>(\n                     std::chrono::seconds{i->duration_})}\n               : n::duration_t::max();\n  };\n\n  auto const not_better_than_direct = [&](n::routing::journey const& j) {\n    auto const first_leg_offset = utl::visit(\n        j.legs_.front().uses_,\n        [&](n::routing::offset const& o) { return std::optional{o}; });\n\n    auto const last_leg_offset = utl::visit(\n        j.legs_.back().uses_,\n        [&](n::routing::offset const& o) { return std::optional{o}; });\n\n    auto const longer_than_direct = [&](n::routing::offset const& o) {\n      return std::optional{o.duration_ >=\n                           get_direct_duration(o.transport_mode_id_)};\n    };\n\n    return first_leg_offset.and_then(longer_than_direct).value_or(false) ||\n           last_leg_offset.and_then(longer_than_direct).value_or(false) ||\n           (first_leg_offset && last_leg_offset &&\n            first_leg_offset->transport_mode_id_ ==\n                last_leg_offset->transport_mode_id_ &&\n            first_leg_offset->duration_ + last_leg_offset->duration_ >=\n                get_direct_duration(first_leg_offset->transport_mode_id_));\n  };\n\n  utl::erase_if(journeys, not_better_than_direct);\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/elevators/elevators.cc",
    "content": "#include \"motis/elevators/elevators.h\"\n\n#include \"osr/ways.h\"\n\nnamespace motis {\n\nvector_map<elevator_idx_t, elevator> update_elevator_coordinates(\n    osr::ways const& w,\n    elevator_id_osm_mapping_t const* ids,\n    hash_set<osr::node_idx_t> const& elevator_nodes,\n    vector_map<elevator_idx_t, elevator>&& elevators) {\n  if (ids != nullptr) {\n    auto id_to_elevator = hash_map<std::string, elevator_idx_t>{};\n    for (auto const [i, e] : utl::enumerate(elevators)) {\n      if (e.id_str_.has_value()) {\n        id_to_elevator.emplace(*e.id_str_, elevator_idx_t{i});\n      }\n    }\n\n    for (auto const n : elevator_nodes) {\n      auto const id_it = ids->find(cista::to_idx(w.node_to_osm_[n]));\n      if (id_it != end(*ids)) {\n        auto const e_it = id_to_elevator.find(id_it->second);\n        if (e_it != end(id_to_elevator)) {\n          elevators[e_it->second].pos_ = w.get_node_pos(n).as_latlng();\n        }\n      }\n    }\n  }\n  return elevators;\n}\n\nelevators::elevators(osr::ways const& w,\n                     elevator_id_osm_mapping_t const* ids,\n                     hash_set<osr::node_idx_t> const& elevator_nodes,\n                     vector_map<elevator_idx_t, elevator>&& elevators)\n    : elevators_{update_elevator_coordinates(\n          w, ids, elevator_nodes, std::move(elevators))},\n      elevators_rtree_{create_elevator_rtree(elevators_)},\n      blocked_{get_blocked_elevators(\n          w, ids, elevators_, elevators_rtree_, elevator_nodes)} {}\n\n}  // namespace motis"
  },
  {
    "path": "src/elevators/match_elevators.cc",
    "content": "#include \"motis/elevators/match_elevator.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/parallel_for.h\"\n\n#include \"osr/ways.h\"\n\nnamespace motis {\n\npoint_rtree<elevator_idx_t> create_elevator_rtree(\n    vector_map<elevator_idx_t, elevator> const& elevators) {\n  auto t = point_rtree<elevator_idx_t>{};\n  for (auto const [i, e] : utl::enumerate(elevators)) {\n    t.add(e.pos_, elevator_idx_t{i});\n  }\n  return t;\n}\n\nosr::hash_set<osr::node_idx_t> get_elevator_nodes(osr::ways const& w) {\n  auto nodes = osr::hash_set<osr::node_idx_t>{};\n  for (auto way = osr::way_idx_t{0U}; way != w.n_ways(); ++way) {\n    for (auto const n : w.r_->way_nodes_[way]) {\n      if (w.r_->node_properties_[n].is_elevator()) {\n        nodes.emplace(n);\n      }\n    }\n  }\n  return nodes;\n}\n\nelevator_idx_t match_elevator(\n    point_rtree<elevator_idx_t> const& rtree,\n    vector_map<elevator_idx_t, elevator> const& elevators,\n    osr::ways const& w,\n    osr::node_idx_t const n) {\n  auto const pos = w.get_node_pos(n).as_latlng();\n  auto closest = elevator_idx_t::invalid();\n  auto closest_dist = std::numeric_limits<double>::max();\n  rtree.find(geo::box{pos, 20.0}, [&](elevator_idx_t const e) {\n    auto const dist = geo::distance(elevators[e].pos_, pos);\n    if (dist < 20 && dist < closest_dist) {\n      closest_dist = dist;\n      closest = e;\n    }\n  });\n  return closest;\n}\n\nosr::bitvec<osr::node_idx_t> get_blocked_elevators(\n    osr::ways const& w,\n    elevator_id_osm_mapping_t const* ids,\n    vector_map<elevator_idx_t, elevator> const& elevators,\n    point_rtree<elevator_idx_t> const& elevators_rtree,\n    osr::hash_set<osr::node_idx_t> const& elevator_nodes) {\n  auto inactive = osr::hash_set<osr::node_idx_t>{};\n  auto inactive_mutex = std::mutex{};\n\n  auto id_to_elevator = hash_map<std::string, elevator_idx_t>{};\n  for (auto const [i, e] : utl::enumerate(elevators)) {\n    if (e.id_str_.has_value()) {\n      id_to_elevator.emplace(*e.id_str_, elevator_idx_t{i});\n    }\n  }\n\n  utl::parallel_for(elevator_nodes, [&](osr::node_idx_t const n) {\n    auto e = elevator_idx_t::invalid();\n\n    // SIRI-FM matching by DIID:\n    // node idx -> OSM ID -> DIID -> elevator_idx\n    if (ids != nullptr) {\n      auto const id_it = ids->find(to_idx(w.node_to_osm_[n]));\n      if (id_it != end(*ids)) {\n        auto const e_it = id_to_elevator.find(id_it->second);\n        if (e_it != end(id_to_elevator)) {\n          e = e_it->second;\n        }\n      }\n    }\n\n    // DB FaSta API: geomatching\n    if (e == elevator_idx_t::invalid()) {\n      e = match_elevator(elevators_rtree, elevators, w, n);\n    }\n\n    if (e != elevator_idx_t::invalid() && !elevators[e].status_) {\n      auto const lock = std::scoped_lock{inactive_mutex};\n      inactive.emplace(n);\n    }\n  });\n  auto blocked = osr::bitvec<osr::node_idx_t>{};\n  blocked.resize(w.n_nodes());\n  for (auto const n : inactive) {\n    blocked.set(n, true);\n  }\n  return blocked;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/elevators/parse_elevator_id_osm_mapping.cc",
    "content": "#include \"motis/elevators/parse_elevator_id_osm_mapping.h\"\n\n#include \"utl/parser/csv_range.h\"\n\nnamespace motis {\n\nelevator_id_osm_mapping_t parse_elevator_id_osm_mapping(std::string_view s) {\n  struct row {\n    utl::csv_col<utl::cstr, UTL_NAME(\"dhid\")> dhid_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"diid\")> diid_;\n    utl::csv_col<utl::cstr, UTL_NAME(\"osm_kind\")> osm_kind_;\n    utl::csv_col<std::uint64_t, UTL_NAME(\"osm_id\")> osm_id_;\n  };\n  auto map = elevator_id_osm_mapping_t{};\n  utl::for_each_row<row>(s, [&](row const& r) {\n    map.emplace(*r.osm_id_, std::string{r.diid_->view()});\n  });\n  return map;\n}\n\nelevator_id_osm_mapping_t parse_elevator_id_osm_mapping(\n    std::filesystem::path const& p) {\n  return parse_elevator_id_osm_mapping(\n      cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}\n          .view());\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/elevators/parse_fasta.cc",
    "content": "#include \"motis/elevators/parse_fasta.h\"\n\n#include <iostream>\n\n#include \"boost/json.hpp\"\n\n#include \"date/date.h\"\n\n#include \"utl/enumerate.h\"\n\nnamespace n = nigiri;\nnamespace json = boost::json;\n\nnamespace motis {\n\nn::unixtime_t parse_date_time(std::string_view s) {\n  auto t = n::unixtime_t{};\n  auto ss = std::stringstream{};\n  ss.exceptions(std::ios_base::failbit | std::ios_base::badbit);\n  ss << s;\n  ss >> date::parse(\"%FT%T\", t);\n  return t;\n}\n\nstd::vector<n::interval<n::unixtime_t>> parse_out_of_service(\n    json::object const& o) {\n  auto ret = std::vector<n::interval<n::unixtime_t>>{};\n\n  if (!o.contains(\"outOfService\")) {\n    return ret;\n  }\n\n  for (auto const& entry : o.at(\"outOfService\").as_array()) {\n    auto const& interval = entry.as_array();\n    if (interval.size() != 2U ||\n        !(interval[0].is_string() && interval[1].is_string())) {\n      fmt::println(\"skip: unable to parse out of service interval {}\",\n                   json::serialize(entry));\n      continue;\n    }\n    ret.emplace_back(n::interval{parse_date_time(interval[0].as_string()),\n                                 parse_date_time(interval[1].as_string())});\n  }\n\n  return ret;\n}\n\nstd::optional<elevator> parse_elevator(json::value const& e) {\n  if (e.at(\"type\") != \"ELEVATOR\") {\n    return std::nullopt;\n  }\n\n  try {\n    auto const& o = e.as_object();\n\n    if (!o.contains(\"geocoordY\") || !o.contains(\"geocoordX\") ||\n        !o.contains(\"state\")) {\n      std::cout << \"skip: missing attributes: \" << o << \"\\n\";\n      return std::nullopt;\n    }\n\n    auto const id = o.contains(\"equipmentnumber\")\n                        ? e.at(\"equipmentnumber\").to_number<std::int64_t>()\n                        : 0U;\n    return elevator{id,\n                    std::nullopt,\n                    geo::latlng{e.at(\"geocoordY\").to_number<double>(),\n                                e.at(\"geocoordX\").to_number<double>()},\n                    e.at(\"state\").as_string() != \"INACTIVE\",\n                    o.contains(\"description\")\n                        ? std::string{o.at(\"description\").as_string()}\n                        : \"\",\n                    parse_out_of_service(o)};\n  } catch (std::exception const& ex) {\n    std::cout << \"error on value: \" << e << \": \" << ex.what() << \"\\n\";\n    return std::nullopt;\n  }\n}\n\nvector_map<elevator_idx_t, elevator> parse_fasta(std::string_view s) {\n  auto ret = vector_map<elevator_idx_t, elevator>{};\n  for (auto const [i, e] : utl::enumerate(json::parse(s).as_array())) {\n    if (auto x = parse_elevator(e); x.has_value()) {\n      ret.emplace_back(std::move(*x));\n    }\n  }\n  return ret;\n}\n\nvector_map<elevator_idx_t, elevator> parse_fasta(\n    std::filesystem::path const& p) {\n  return parse_fasta(\n      cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}\n          .view());\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/elevators/parse_siri_fm.cc",
    "content": "#include \"motis/elevators/parse_siri_fm.h\"\n\n#include \"pugixml.hpp\"\n\nnamespace motis {\n\nstd::optional<elevator> parse_facility_condition(pugi::xml_node const& fc) {\n  auto const id = fc.child_value(\"FacilityRef\");\n  if (id == nullptr || *id == '\\0') {\n    return std::nullopt;\n  }\n\n  auto const status = fc.child(\"FacilityStatus\").child_value(\"Status\");\n  if (status == nullptr || *status == '\\0') {\n    return std::nullopt;\n  }\n\n  return elevator{\n      .id_ = 0U,\n      .id_str_ = std::string{id},\n      .pos_ = geo::latlng{},\n      .status_ = std::string_view{status} == \"available\",\n      .desc_ = \"\",\n      .out_of_service_ = {},\n  };\n}\n\nvector_map<elevator_idx_t, elevator> parse_siri_fm(std::string_view s) {\n  auto doc = pugi::xml_document{};\n  if (!doc.load_buffer(s.data(), s.size())) {\n    return {};\n  }\n\n  auto ret = vector_map<elevator_idx_t, elevator>{};\n  for (auto fc : doc.child(\"Siri\")\n                     .child(\"ServiceDelivery\")\n                     .child(\"FacilityMonitoringDelivery\")\n                     .children(\"FacilityCondition\")) {\n    if (auto e = parse_facility_condition(fc); e.has_value()) {\n      ret.emplace_back(std::move(*e));\n    }\n  }\n  return ret;\n}\n\nvector_map<elevator_idx_t, elevator> parse_siri_fm(\n    std::filesystem::path const& p) {\n  return parse_siri_fm(\n      cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ}\n          .view());\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/elevators/update_elevators.cc",
    "content": "#include \"motis/elevators/update_elevators.h\"\n\n#include \"utl/verify.h\"\n\n#include \"nigiri/logging.h\"\n\n#include \"motis/config.h\"\n#include \"motis/constants.h\"\n#include \"motis/data.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/elevators/parse_fasta.h\"\n#include \"motis/elevators/parse_siri_fm.h\"\n#include \"motis/update_rtt_td_footpaths.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nusing elevator_map_t = hash_map<std::int64_t, elevator_idx_t>;\n\nelevator_map_t to_map(vector_map<elevator_idx_t, elevator> const& elevators) {\n  auto m = elevator_map_t{};\n  for (auto const [i, e] : utl::enumerate(elevators)) {\n    m.emplace(e.id_, elevator_idx_t{i});\n  }\n  return m;\n}\n\nptr<elevators> update_elevators(config const& c,\n                                data const& d,\n                                std::string_view body,\n                                n::rt_timetable& new_rtt) {\n  auto new_e = std::make_unique<elevators>(\n      *d.w_, d.elevator_osm_mapping_.get(), *d.elevator_nodes_,\n      body.contains(\"<Siri\") ? parse_siri_fm(body) : parse_fasta(body));\n  auto const& old_e = *d.rt_->e_;\n  auto const old_map = to_map(old_e.elevators_);\n  auto const new_map = to_map(new_e->elevators_);\n\n  auto tasks = hash_set<std::pair<n::location_idx_t, osr::direction>>{};\n  auto const add_tasks = [&](std::optional<geo::latlng> const& pos) {\n    if (!pos.has_value()) {\n      return;\n    }\n    d.location_rtree_->in_radius(*pos, kElevatorUpdateRadius,\n                                 [&](n::location_idx_t const l) {\n                                   tasks.emplace(l, osr::direction::kForward);\n                                   tasks.emplace(l, osr::direction::kBackward);\n                                 });\n  };\n\n  for (auto const& [id, e_idx] : old_map) {\n    auto const it = new_map.find(id);\n    if (it == end(new_map)) {\n      // Elevator got removed.\n      // Not listed in new => default status = ACTIVE\n      // Update if INACTIVE before (= status changed)\n      if (old_e.elevators_[e_idx].status_ == false) {\n        add_tasks(old_e.elevators_[e_idx].pos_);\n      }\n    } else {\n      // Elevator remained. Update if status changed.\n      if (new_e->elevators_[it->second].status_ !=\n          old_e.elevators_[e_idx].status_) {\n        add_tasks(new_e->elevators_[it->second].pos_);\n      }\n    }\n  }\n\n  for (auto const& [id, e_idx] : new_map) {\n    auto const it = old_map.find(id);\n    if (it == end(old_map) && new_e->elevators_[e_idx].status_ == false) {\n      // New elevator not seen before, elevator is NOT working. Update.\n      add_tasks(new_e->elevators_[e_idx].pos_);\n    }\n  }\n\n  n::log(n::log_lvl::info, \"motis.rt.elevators\",\n         \"elevator update: {} routing tasks\", tasks.size());\n\n  update_rtt_td_footpaths(\n      *d.w_, *d.l_, *d.pl_, *d.tt_, *d.location_rtree_, *new_e, *d.matches_,\n      tasks, d.rt_->rtt_.get(), new_rtt,\n      std::chrono::seconds{c.timetable_.value().max_footpath_length_ * 60});\n\n  return new_e;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/endpoints/adr/filter_conv.cc",
    "content": "#include \"motis/endpoints/adr/filter_conv.h\"\n\nnamespace a = adr;\n\nnamespace motis {\n\nadr::filter_type to_filter_type(\n    std::optional<motis::api::LocationTypeEnum> const& f) {\n  if (f.has_value()) {\n    switch (*f) {\n      case api::LocationTypeEnum::ADDRESS: return a::filter_type::kAddress;\n      case api::LocationTypeEnum::PLACE: return a::filter_type::kPlace;\n      case api::LocationTypeEnum::STOP: return a::filter_type::kExtra;\n    }\n  }\n  return a::filter_type::kNone;\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/endpoints/adr/geocode.cc",
    "content": "#include \"motis/endpoints/adr/geocode.h\"\n\n#include \"boost/thread/tss.hpp\"\n\n#include \"utl/for_each_bit_set.h\"\n#include \"utl/to_vec.h\"\n\n#include \"fmt/format.h\"\n\n#include \"net/bad_request_exception.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"adr/adr.h\"\n#include \"adr/typeahead.h\"\n\n#include \"motis/config.h\"\n#include \"motis/endpoints/adr/filter_conv.h\"\n#include \"motis/endpoints/adr/suggestions_to_response.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/timetable/modes_to_clasz_mask.h\"\n\nnamespace n = nigiri;\nnamespace a = adr;\n\nnamespace motis::ep {\n\nconstexpr auto const kDefaultSuggestions = 10U;\n\na::guess_context& get_guess_context(a::typeahead const& t, a::cache& cache) {\n  auto static ctx = boost::thread_specific_ptr<a::guess_context>{};\n  if (ctx.get() == nullptr || &ctx.get()->cache_ != &cache) {\n    ctx.reset(new a::guess_context{cache});\n  }\n  ctx->resize(t);\n  return *ctx;\n}\n\napi::geocode_response geocode::operator()(\n    boost::urls::url_view const& url) const {\n  auto const params = api::geocode_params{url.params()};\n  auto const place = params.place_.and_then([](std::string const& s) {\n    auto const parsed = parse_location(s);\n    utl::verify<net::bad_request_exception>(parsed.has_value(),\n                                            \"could not parse place {}\", s);\n    return std::optional{parsed.value().pos_};\n  });\n  auto const required_modes =\n      params.mode_.transform([](std::vector<api::ModeEnum> const& modes) {\n        return to_clasz_mask(modes);\n      });\n\n  auto& ctx = get_guess_context(t_, cache_);\n\n  auto lang_indices = basic_string<a::language_idx_t>{{a::kDefaultLang}};\n  if (params.language_.has_value()) {\n    for (auto const& language : *params.language_) {\n      auto const l_idx = t_.resolve_language(language);\n      if (l_idx != a::language_idx_t::invalid()) {\n        lang_indices.push_back(l_idx);\n      }\n    }\n  }\n  auto const place_filter =\n      required_modes\n          .and_then(\n              [&](n::routing::clasz_mask_t const required_clasz)\n                  -> std::optional<std::function<bool(adr::place_idx_t)>> {\n                if (required_clasz == 0U) {\n                  return std::nullopt;\n                }\n                return {std::function{\n                    [&, required_clasz](adr::place_idx_t place_idx) {\n                      if (t_.place_type_[place_idx] !=\n                          adr::amenity_category::kExtra) {\n                        return true;\n                      }\n                      auto const i = adr_extra_place_idx_t{\n                          static_cast<adr_extra_place_idx_t::value_t>(\n                              place_idx - t_.ext_start_)};\n                      return (ae_->place_clasz_.at(i) & required_clasz) != 0U;\n                    }}};\n              })\n          .value_or(std::function<bool(adr::place_idx_t)>{});\n  auto const config_limit = config_.get_limits().geocode_max_suggestions_;\n  auto const requested_limit = params.numResults_.value_or(kDefaultSuggestions);\n  utl::verify<net::bad_request_exception>(requested_limit >= 1,\n                                          \"limit must be >= 1\");\n  utl::verify<net::bad_request_exception>(\n      requested_limit <= config_limit,\n      \"limit must be <= geocode_max_suggestions ({})\", config_limit);\n  auto const token_pos = a::get_suggestions<false>(\n      t_, params.text_, static_cast<unsigned>(requested_limit), lang_indices,\n      ctx, place, static_cast<float>(params.placeBias_),\n      to_filter_type(params.type_), place_filter);\n  return suggestions_to_response(t_, f_, ae_, tt_, tags_, w_, pl_, matches_,\n                                 lang_indices, token_pos, ctx.suggestions_);\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/adr/reverse_geocode.cc",
    "content": "#include \"motis/endpoints/adr/reverse_geocode.h\"\n\n#include \"net/bad_request_exception.h\"\n\n#include \"adr/guess_context.h\"\n#include \"adr/reverse.h\"\n\n#include \"motis/config.h\"\n#include \"motis/endpoints/adr/filter_conv.h\"\n#include \"motis/endpoints/adr/suggestions_to_response.h\"\n#include \"motis/parse_location.h\"\n\nnamespace a = adr;\n\nnamespace motis::ep {\n\nconstexpr auto const kDefaultResults = 5U;\n\napi::reverseGeocode_response reverse_geocode::operator()(\n    boost::urls::url_view const& url) const {\n  auto const params = api::reverseGeocode_params{url.params()};\n  auto const config_limit = config_.get_limits().reverse_geocode_max_results_;\n  auto const requested_limit = params.numResults_.value_or(kDefaultResults);\n  utl::verify<net::bad_request_exception>(requested_limit >= 1,\n                                          \"limit must be >= 1\");\n  utl::verify<net::bad_request_exception>(\n      requested_limit <= config_limit,\n      \"limit must be <= reverse_geocode_max_results ({})\", config_limit);\n  return suggestions_to_response(\n      t_, f_, ae_, tt_, tags_, w_, pl_, matches_, {}, {},\n      r_.lookup(t_, parse_location((params.place_))->pos_,\n                static_cast<std::size_t>(requested_limit),\n                to_filter_type(params.type_)));\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/adr/suggestions_to_response.cc",
    "content": "#include \"motis/endpoints/adr/suggestions_to_response.h\"\n\n#include \"utl/for_each_bit_set.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/overloaded.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/visit.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"adr/typeahead.h\"\n\n#include \"motis/journey_to_response.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/clasz_to_mode.h\"\n\nnamespace a = adr;\nnamespace n = nigiri;\n\nnamespace motis {\n\nlong get_area_lang_idx(a::typeahead const& t,\n                       a::language_list_t const& languages,\n                       a::area_idx_t const a) {\n  for (auto i = 0U; i != languages.size(); ++i) {\n    auto const j = languages.size() - i - 1U;\n    auto const lang_idx = a::find_lang(t.area_name_lang_[a], languages[j]);\n    if (lang_idx != -1) {\n      return lang_idx;\n    }\n  }\n  return -1;\n}\n\napi::geocode_response suggestions_to_response(\n    adr::typeahead const& t,\n    adr::formatter const& f,\n    adr_ext const* ae,\n    n::timetable const* tt,\n    tag_lookup const* tags,\n    osr::ways const* w,\n    osr::platforms const* pl,\n    platform_matches_t const* matches,\n    basic_string<a::language_idx_t> const& lang_indices,\n    std::vector<adr::token> const& token_pos,\n    std::vector<adr::suggestion> const& suggestions) {\n  return utl::to_vec(suggestions, [&](a::suggestion const& s) {\n    auto const areas = t.area_sets_[s.area_set_];\n    auto modes = std::optional<std::vector<api::ModeEnum>>{};\n    auto importance = std::optional<double>{};\n    auto type = api::LocationTypeEnum{};\n    auto street = std::optional<std::string>{};\n    auto house_number = std::optional<std::string>{};\n    auto id = std::string{};\n    auto level = std::optional<double>{};\n    auto category = std::optional<std::string>{};\n    utl::visit(\n        s.location_,\n        [&](a::place_idx_t const p) {\n          type = t.place_type_[p] == a::amenity_category::kExtra\n                     ? api::LocationTypeEnum::STOP\n                     : api::LocationTypeEnum::PLACE;\n          if (type == api::LocationTypeEnum::STOP) {\n            if (tt != nullptr && tags != nullptr) {\n              auto const l = n::location_idx_t{\n                  static_cast<n::location_idx_t::value_t>(s.get_osm_id(t))};\n              level = get_level(w, pl, matches, l);\n              id = tags->id(*tt, l);\n            } else {\n              id = fmt::format(\"stop/{}\", p);\n            }\n\n            if (ae != nullptr) {\n              auto const i = adr_extra_place_idx_t{\n                  static_cast<adr_extra_place_idx_t::value_t>(p -\n                                                              t.ext_start_)};\n              modes = to_modes(ae->place_clasz_[i], 5);\n              importance = ae->place_importance_[i];\n            }\n          } else {\n            category = to_str(t.place_type_[p]);\n            id = fmt::format(\"{}/{}\",\n                             t.place_is_way_[to_idx(p)] ? \"way\" : \"node\",\n                             t.place_osm_ids_[p]);\n          }\n          return std::string{t.strings_[s.str_].view()};\n        },\n        [&](a::address const addr) {\n          type = api::LocationTypeEnum::ADDRESS;\n          if (addr.house_number_ != a::address::kNoHouseNumber) {\n            street = t.strings_[s.str_].view();\n            house_number =\n                t.strings_[t.house_numbers_[addr.street_][addr.house_number_]]\n                    .view();\n            return fmt::format(\"{} {}\", *street, *house_number);\n          } else {\n            return std::string{t.strings_[s.str_].view()};\n          }\n        });\n\n    auto tokens = std::vector<std::vector<double>>{};\n    utl::for_each_set_bit(s.matched_tokens_, [&](auto const i) {\n      assert(i < token_pos.size());\n      tokens.emplace_back(\n          std::vector<double>{static_cast<double>(token_pos[i].start_idx_),\n                              static_cast<double>(token_pos[i].size_)});\n    });\n\n    auto const is_matched = [&](std::size_t const i) {\n      return (((1U << i) & s.matched_areas_) != 0U);\n    };\n\n    auto api_areas = std::vector<api::Area>{};\n    for (auto const [i, a] : utl::enumerate(areas)) {\n      auto const admin_lvl = t.area_admin_level_[a];\n      if (admin_lvl == a::kPostalCodeAdminLevel ||\n          admin_lvl == a::kTimezoneAdminLevel) {\n        continue;\n      }\n\n      auto const language = is_matched(i)\n                                ? s.matched_area_lang_[i]\n                                : get_area_lang_idx(t, lang_indices, a);\n      auto const area_name =\n          t.strings_[t.area_names_[a][language == -1\n                                          ? a::kDefaultLangIdx\n                                          : static_cast<unsigned>(language)]]\n              .view();\n      api_areas.emplace_back(api::Area{\n          .name_ = std::string{area_name},\n          .adminLevel_ = static_cast<double>(to_idx(admin_lvl)),\n          .matched_ = is_matched(i),\n          .unique_ = s.unique_area_idx_.has_value() && *s.unique_area_idx_ == i,\n          .default_ = s.city_area_idx_.has_value() && *s.city_area_idx_ == i});\n    }\n\n    auto const country_code = s.get_country_code(t);\n\n    return api::Match{\n        .type_ = type,\n        .category_ = std::move(category),\n        .tokens_ = std::move(tokens),\n        .name_ = s.format(t, f, country_code.value_or(\"DE\")),\n        .id_ = std::move(id),\n        .lat_ = s.coordinates_.as_latlng().lat_,\n        .lon_ = s.coordinates_.as_latlng().lng_,\n        .level_ = level,\n        .street_ = std::move(street),\n        .houseNumber_ = std::move(house_number),\n        .country_ = country_code.and_then(\n            [](std::string_view s) { return std::optional{std::string{s}}; }),\n        .zip_ = s.zip_area_idx_.and_then([&](unsigned const zip_area_idx) {\n          return std::optional<std::string>{\n              t.strings_[t.area_names_[areas[zip_area_idx]][a::kDefaultLangIdx]]\n                  .view()};\n        }),\n        .tz_ =\n            s.tz_ == a::timezone_idx_t::invalid()\n                ? std::nullopt\n                : std::optional{std::string{t.timezone_names_[s.tz_].view()}},\n        .areas_ = std::move(api_areas),\n        .score_ = s.score_,\n        .modes_ = std::move(modes),\n        .importance_ = importance};\n  });\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/endpoints/elevators.cc",
    "content": "#include \"motis/endpoints/elevators.h\"\n\n#include \"net/too_many_exception.h\"\n\n#include \"osr/geojson.h\"\n\n#include \"boost/json.hpp\"\n\n#include \"fmt/chrono.h\"\n#include \"fmt/format.h\"\n\n#include \"motis/data.h\"\n#include \"motis/elevators/match_elevator.h\"\n\nnamespace json = boost::json;\nnamespace n = nigiri;\n\nnamespace std {\n\nn::unixtime_t tag_invoke(boost::json::value_to_tag<n::unixtime_t>,\n                         boost::json::value const& jv) {\n  auto x = n::unixtime_t{};\n  auto ss = std::stringstream{std::string{jv.as_string()}};\n  ss >> date::parse(\"%FT%T\", x);\n  return x;\n}\n\nvoid tag_invoke(boost::json::value_from_tag,\n                boost::json::value& jv,\n                n::unixtime_t const& v) {\n  auto ss = std::stringstream{};\n  ss << date::format(\"%FT%TZ\", v);\n  jv = json::string{ss.str()};\n}\n\n}  // namespace std\n\nnamespace nigiri {\n\ntemplate <typename T>\nn::interval<T> tag_invoke(boost::json::value_to_tag<n::interval<T>>,\n                          boost::json::value const& jv) {\n  auto x = n::interval<T>{};\n  x.from_ = json::value_to<T>(jv.as_array().at(0));\n  x.to_ = json::value_to<T>(jv.as_array().at(1));\n  return x;\n}\n\ntemplate <typename T>\nvoid tag_invoke(boost::json::value_from_tag,\n                boost::json::value& jv,\n                n::interval<T> const& v) {\n  auto& a = (jv = boost::json::array{}).as_array();\n  a.emplace_back(json::value_from(v.from_));\n  a.emplace_back(json::value_from(v.to_));\n}\n\n}  // namespace nigiri\n\nnamespace motis::ep {\n\nconstexpr auto const kLimit = 4096U;\n\njson::value elevators::operator()(json::value const& query) const {\n  auto const rt = std::atomic_load(&rt_);\n  auto const e = rt->e_.get();\n\n  auto matches = json::array{};\n  if (e == nullptr) {\n    return json::value{{\"type\", \"FeatureCollection\"}, {\"features\", matches}};\n  }\n\n  auto const& q = query.as_array();\n\n  auto const min = geo::latlng{q[1].as_double(), q[0].as_double()};\n  auto const max = geo::latlng{q[3].as_double(), q[2].as_double()};\n\n  e->elevators_rtree_.find(geo::box{min, max}, [&](elevator_idx_t const i) {\n    utl::verify<net::too_many_exception>(matches.size() < kLimit,\n                                         \"too many elevators\");\n    auto const& x = e->elevators_[i];\n    matches.emplace_back(json::value{\n        {\"type\", \"Feature\"},\n        {\"properties\",\n         {{\"type\", \"api\"},\n          {\"id\", x.id_},\n          {\"desc\", x.desc_},\n          {\"status\", (x.status_ ? \"ACTIVE\" : \"INACTIVE\")},\n          {\"outOfService\", json::value_from(x.out_of_service_)}}},\n        {\"geometry\", osr::to_point(osr::point::from_latlng(x.pos_))}});\n  });\n\n  for (auto const n : l_.find_elevators({min, max})) {\n    auto const match =\n        match_elevator(e->elevators_rtree_, e->elevators_, w_, n);\n    auto const pos = w_.get_node_pos(n);\n    if (match != elevator_idx_t::invalid()) {\n      auto const& x = e->elevators_[match];\n\n      utl::verify<net::too_many_exception>(matches.size() < kLimit,\n                                           \"too many elevators\");\n      matches.emplace_back(json::value{\n          {\"type\", \"Feature\"},\n          {\"properties\",\n           {{\"type\", \"match\"},\n            {\"osm_node_id\", to_idx(w_.node_to_osm_[n])},\n            {\"id\", x.id_},\n            {\"desc\", x.desc_},\n            {\"status\", x.status_ ? \"ACTIVE\" : \"INACTIVE\"},\n            {\"outOfService\", json::value_from(x.out_of_service_)}}},\n          {\"geometry\",\n           osr::to_line_string({pos, osr::point::from_latlng(x.pos_)})}});\n    }\n  }\n\n  return json::value{{\"type\", \"FeatureCollection\"}, {\"features\", matches}};\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/graph.cc",
    "content": "#include \"motis/endpoints/graph.h\"\n\n#include \"net/too_many_exception.h\"\n\n#include \"osr/geojson.h\"\n#include \"osr/routing/profiles/car_sharing.h\"\n#include \"osr/routing/route.h\"\n\nnamespace json = boost::json;\n\nnamespace motis::ep {\n\nconstexpr auto const kMaxWays = 2048U;\n\njson::value graph::operator()(json::value const& query) const {\n  auto const& q = query.as_object();\n  auto const& x = query.at(\"waypoints\").as_array();\n  auto const min = geo::latlng{x[1].as_double(), x[0].as_double()};\n  auto const max = geo::latlng{x[3].as_double(), x[2].as_double()};\n  auto const level = q.contains(\"level\")\n                         ? osr::level_t{q.at(\"level\").to_number<float>()}\n                         : osr::kNoLevel;\n\n  auto gj = osr::geojson_writer{.w_ = w_};\n  auto n_ways = 0U;\n  l_.find({min, max}, [&](osr::way_idx_t const w) {\n    if (++n_ways == kMaxWays) {\n      throw utl::fail<net::too_many_exception>(\"too many ways\");\n    }\n\n    if (level == osr::kNoLevel) {\n      gj.write_way(w);\n      return;\n    }\n\n    auto const way_prop = w_.r_->way_properties_[w];\n    if (way_prop.is_elevator()) {\n      auto const n = w_.r_->way_nodes_[w][0];\n      auto const np = w_.r_->node_properties_[n];\n      if (np.is_multi_level()) {\n        auto has_level = false;\n        utl::for_each_set_bit(\n            osr::foot<true>::get_elevator_multi_levels(*w_.r_, n),\n            [&](auto&& bit) {\n              has_level |=\n                  (level == osr::level_t{static_cast<std::uint8_t>(bit)});\n            });\n        if (has_level) {\n          gj.write_way(w);\n          return;\n        }\n      }\n    }\n\n    if ((level == osr::level_t{0.F} &&\n         way_prop.from_level() == osr::kNoLevel) ||\n        way_prop.from_level() == level || way_prop.to_level() == level) {\n      gj.write_way(w);\n      return;\n    }\n  });\n\n  gj.finish(&osr::get_dijkstra<osr::car_sharing<osr::track_node_tracking>>());\n\n  return gj.json();\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/gtfsrt.cc",
    "content": "#include \"motis/endpoints/gtfsrt.h\"\n\n#include <functional>\n\n#ifdef NO_DATA\n#undef NO_DATA\n#endif\n#include \"gtfsrt/gtfs-realtime.pb.h\"\n\n#include \"utl/enumerate.h\"\n\n#include \"net/too_many_exception.h\"\n\n#include \"nigiri/loader/gtfs/stop_seq_number_encoding.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/timetable.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/data.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace n = nigiri;\n\nnamespace gtfsrt = transit_realtime;\nnamespace protob = google::protobuf;\n\nnamespace motis::ep {\n\nvoid add_trip_updates(n::timetable const& tt,\n                      tag_lookup const& tags,\n                      nigiri::rt::frun const& fr,\n                      transit_realtime::FeedMessage& fm) {\n  fr.for_each_trip([&](n::trip_idx_t trip_idx,\n                       n::interval<n::stop_idx_t> const subrange) {\n    auto fe = fm.add_entity();\n    auto const trip_id = tags.id_fragments(\n        tt, fr[subrange.from_ - fr.stop_range_.from_], n::event_type::kDep);\n    fe->set_id(trip_id.trip_id_);\n    auto tu = fe->mutable_trip_update();\n    auto td = tu->mutable_trip();\n    td->set_trip_id(trip_id.trip_id_);\n    td->set_start_time(fmt::format(\"{}:00\", trip_id.start_time_));\n    td->set_start_date(trip_id.start_date_);\n    td->set_schedule_relationship(\n        fr.is_cancelled()\n            ? transit_realtime::TripDescriptor_ScheduleRelationship::\n                  TripDescriptor_ScheduleRelationship_CANCELED\n        : !fr.is_scheduled()\n            ? transit_realtime::TripDescriptor_ScheduleRelationship::\n                  TripDescriptor_ScheduleRelationship_ADDED\n            : transit_realtime::TripDescriptor_ScheduleRelationship::\n                  TripDescriptor_ScheduleRelationship_SCHEDULED);\n    if (!fr.is_scheduled()) {\n      auto const route_id_idx = fr.rtt_->rt_transport_route_id_.at(fr.rt_);\n      if (route_id_idx != n::route_id_idx_t::invalid()) {\n        td->set_route_id(\n            tt.route_ids_[fr.rtt_->rt_transport_src_.at(fr.rt_)].ids_.get(\n                route_id_idx));\n      }\n    }\n    if (fr.is_cancelled()) {\n      return;\n    }\n    auto const seq_numbers =\n        fr.is_scheduled()\n            ? n::loader::gtfs::\n                  stop_seq_number_range{{tt.trip_stop_seq_numbers_[trip_idx]},\n                                        static_cast<n::stop_idx_t>(\n                                            subrange.size())}\n            : n::loader::gtfs::stop_seq_number_range{\n                  std::span<n::stop_idx_t>{},\n                  static_cast<n::stop_idx_t>(fr.size())};\n    auto stop_idx =\n        fr.is_scheduled() ? subrange.from_ : static_cast<unsigned short>(0U);\n    auto seq_it = begin(seq_numbers);\n\n    auto last_delay = n::duration_t::max();\n    for (; seq_it != end(seq_numbers); ++stop_idx, ++seq_it) {\n      auto const s = fr[stop_idx - fr.stop_range_.from_];\n\n      transit_realtime::TripUpdate_StopTimeUpdate* stu = nullptr;\n      auto const set_stu = [&]() {\n        if (stu != nullptr) {\n          return;\n        }\n        stu = tu->add_stop_time_update();\n        stu->set_stop_id(\n            tt.locations_.ids_[s.get_stop().location_idx()].view());\n        stu->set_stop_sequence(*seq_it);\n      };\n\n      auto const to_unix = [&](n::unixtime_t t) {\n        return std::chrono::time_point_cast<std::chrono::seconds>(t)\n            .time_since_epoch()\n            .count();\n      };\n      auto const to_delay_seconds = [&](n::duration_t t) {\n        return static_cast<int32_t>(\n            std::chrono::duration_cast<std::chrono::seconds>(t).count());\n      };\n\n      if (s.stop_idx_ != 0) {\n        auto const arr_delay = s.delay(nigiri::event_type::kArr);\n        if (arr_delay != last_delay || !fr.is_scheduled()) {\n          set_stu();\n          auto ar = stu->mutable_arrival();\n          ar->set_time(to_unix(s.time(nigiri::event_type::kArr)));\n          ar->set_delay(to_delay_seconds(arr_delay));\n          last_delay = arr_delay;\n        }\n      }\n      if (s.stop_idx_ != fr.size() - 1) {\n        auto const dep_delay = s.delay(nigiri::event_type::kDep);\n        if (dep_delay != last_delay || !fr.is_scheduled()) {\n          set_stu();\n          auto dep = stu->mutable_departure();\n          dep->set_time(to_unix(s.time(nigiri::event_type::kDep)));\n          dep->set_delay(to_delay_seconds(dep_delay));\n          last_delay = dep_delay;\n        }\n      }\n      if (s.is_cancelled() && !s.get_scheduled_stop().is_cancelled()) {\n        set_stu();\n        stu->set_schedule_relationship(\n            transit_realtime::TripUpdate_StopTimeUpdate_ScheduleRelationship::\n                TripUpdate_StopTimeUpdate_ScheduleRelationship_SKIPPED);\n      }\n    }\n  });\n}\n\nvoid add_rt_transports(n::timetable const& tt,\n                       tag_lookup const& tags,\n                       n::rt_timetable const& rtt,\n                       transit_realtime::FeedMessage& fm) {\n  for (auto rt_t = nigiri::rt_transport_idx_t{0}; rt_t < rtt.n_rt_transports();\n       ++rt_t) {\n    auto const fr = n::rt::frun::from_rt(tt, &rtt, rt_t);\n    add_trip_updates(tt, tags, fr, fm);\n  }\n}\n\nvoid add_cancelled_transports(n::timetable const& tt,\n                              tag_lookup const& tags,\n                              n::rt_timetable const& rtt,\n                              transit_realtime::FeedMessage& fm) {\n  auto const start_time = std::max(\n      std::chrono::time_point_cast<n::unixtime_t::duration>(\n          std::chrono::system_clock::now() -\n          std::chrono::duration_cast<n::duration_t>(n::kTimetableOffset)),\n      tt.internal_interval().from_);\n  auto const end_time = std::min(\n      std::chrono::time_point_cast<n::unixtime_t::duration>(\n          start_time +\n          std::chrono::duration_cast<n::duration_t>(std::chrono::days{6})),\n      tt.internal_interval().to_);\n  auto const [start_day, _] = tt.day_idx_mam(start_time);\n  auto const [end_day, _1] = tt.day_idx_mam(end_time);\n\n  for (auto r = nigiri::route_idx_t{0}; r < tt.n_routes(); ++r) {\n    for (auto const [i, t_idx] :\n         utl::enumerate(tt.route_transport_ranges_[r])) {\n      for (auto day = start_day; day <= end_day; ++day) {\n        auto const t = n::transport{t_idx, day};\n        auto const is_cancelled =\n            tt.bitfields_[tt.transport_traffic_days_[t.t_idx_]].test(\n                to_idx(t.day_)) &&\n            !rtt.bitfields_[rtt.transport_traffic_days_[t.t_idx_]].test(\n                to_idx(t.day_));\n        if (!is_cancelled) {\n          continue;\n        }\n        auto fr = n::rt::frun::from_t(tt, &rtt, t);\n        if (fr.is_rt()) {\n          continue;\n        }\n        add_trip_updates(tt, tags, fr, fm);\n      }\n    }\n  }\n}\n\nnet::reply gtfsrt::operator()(net::route_request const& req, bool) const {\n  utl::verify(tt_ != nullptr && tags_ != nullptr, \"no tt initialized\");\n  auto const rt = std::atomic_load(&rt_);\n  auto const rtt = rt->rtt_.get();\n\n  utl::verify<net::too_many_exception>(\n      config_.get_limits().gtfsrt_expose_max_trip_updates_ != 0 &&\n          rtt->n_rt_transports() <\n              config_.get_limits().gtfsrt_expose_max_trip_updates_,\n      \"number of trip updates above configured limit\");\n\n  auto fm = transit_realtime::FeedMessage();\n  auto fh = fm.mutable_header();\n  fh->set_gtfs_realtime_version(\"2.0\");\n  fh->set_incrementality(\n      transit_realtime::FeedHeader_Incrementality_FULL_DATASET);\n  auto const time = std::time(nullptr);\n  fh->set_timestamp(static_cast<double>(time));\n\n  if (rtt != nullptr) {\n    add_rt_transports(*tt_, *tags_, *rtt, fm);\n    add_cancelled_transports(*tt_, *tags_, *rtt, fm);\n  }\n\n  auto res = net::web_server::string_res_t{boost::beast::http::status::ok,\n                                           req.version()};\n  res.insert(boost::beast::http::field::content_type, \"application/x-protobuf\");\n  res.keep_alive(req.keep_alive());\n  set_response_body(res, req, fm.SerializeAsString());\n\n  return res;\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/initial.cc",
    "content": "#include \"motis/endpoints/initial.h\"\n#include \"motis/config.h\"\n\n#include \"utl/erase_if.h\"\n#include \"utl/to_vec.h\"\n\n#include \"tiles/fixed/convert.h\"\n#include \"tiles/fixed/fixed_geometry.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"motis/data.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\napi::initial_response get_initial_response(data const& d) {\n  auto const get_quantiles = [](std::vector<double>&& coords) {\n    utl::erase_if(coords, [](auto const c) { return c == 0.; });\n    if (coords.empty()) {\n      return std::make_pair(0., 0.);\n    }\n    if (coords.size() < 10) {\n      return std::make_pair(coords.front(), coords.back());\n    }\n\n    std::sort(begin(coords), end(coords));\n    constexpr auto const kQuantile = .8;\n    return std::make_pair(\n        coords.at(static_cast<double>(coords.size()) * (1 - kQuantile)),\n        coords.at(static_cast<double>(coords.size()) * (kQuantile)));\n  };\n\n  auto zoom = 0U;\n  auto center = geo::latlng{};\n\n  auto const tt = d.tt_.get();\n  if (tt != nullptr) {\n    auto const [lat_min, lat_max] = get_quantiles(utl::to_vec(\n        tt->locations_.coordinates_, [](auto const& s) { return s.lat_; }));\n    auto const [lng_min, lng_max] = get_quantiles(utl::to_vec(\n        tt->locations_.coordinates_, [](auto const& s) { return s.lng_; }));\n\n    auto const fixed0 = tiles::latlng_to_fixed({lat_min, lng_min});\n    auto const fixed1 = tiles::latlng_to_fixed({lat_max, lng_max});\n\n    center = tiles::fixed_to_latlng(\n        {(fixed0.x() + fixed1.x()) / 2, (fixed0.y() + fixed1.y()) / 2});\n\n    auto const span = static_cast<unsigned>(std::max(\n        std::abs(fixed0.x() - fixed1.x()), std::abs(fixed0.y() - fixed1.y())));\n\n    for (; zoom < (tiles::kMaxZoomLevel - 1); ++zoom) {\n      if (((tiles::kTileSize * 2ULL) *\n           (1ULL << (tiles::kMaxZoomLevel - (zoom + 1)))) < span) {\n        break;\n      }\n    }\n  }\n\n  auto const limits = d.config_.get_limits();\n  return {\n      .lat_ = center.lat_,\n      .lon_ = center.lng_,\n      .zoom_ = static_cast<double>(zoom),\n      .serverConfig_ = api::ServerConfig{\n          .motisVersion_ = std::string{d.motis_version_},\n          .hasElevation_ = d.config_.get_street_routing()\n                               .transform([](config::street_routing const& x) {\n                                 return x.elevation_data_dir_.has_value();\n                               })\n                               .value_or(false),\n          .hasRoutedTransfers_ = d.config_.osr_footpath_,\n          .hasStreetRouting_ = d.config_.get_street_routing().has_value(),\n          .maxOneToManySize_ = static_cast<double>(limits.onetomany_max_many_),\n          .maxOneToAllTravelTimeLimit_ =\n              static_cast<double>(limits.onetoall_max_travel_minutes_),\n          .maxPrePostTransitTimeLimit_ = static_cast<double>(\n              limits.street_routing_max_prepost_transit_seconds_),\n          .maxDirectTimeLimit_ =\n              static_cast<double>(limits.street_routing_max_direct_seconds_),\n          .shapesDebugEnabled_ = d.config_.shapes_debug_api_enabled()}};\n}\n\napi::initial_response initial::operator()(boost::urls::url_view const&) const {\n  return response_;\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/levels.cc",
    "content": "#include \"motis/endpoints/levels.h\"\n\n#include \"net/bad_request_exception.h\"\n\n#include \"utl/pipes/all.h\"\n#include \"utl/pipes/vec.h\"\n#include \"utl/to_vec.h\"\n\n#include \"osr/lookup.h\"\n\n#include \"motis/parse_location.h\"\n#include \"motis/types.h\"\n\nnamespace json = boost::json;\n\nnamespace motis::ep {\n\napi::levels_response levels::operator()(\n    boost::urls::url_view const& url) const {\n  auto const query = api::levels_params{url.params()};\n  auto const min = parse_location(query.min_);\n  auto const max = parse_location(query.max_);\n  utl::verify<net::bad_request_exception>(\n      min.has_value(), \"min not a coordinate: {}\", query.min_);\n  utl::verify<net::bad_request_exception>(\n      max.has_value(), \"max not a coordinate: {}\", query.max_);\n  auto levels = hash_set<float>{};\n  l_.find({min->pos_, max->pos_}, [&](osr::way_idx_t const x) {\n    auto const p = w_.r_->way_properties_[x];\n    levels.emplace(p.from_level().to_float());\n    levels.emplace(p.to_level().to_float());\n  });\n  auto levels_sorted =\n      utl::to_vec(levels, [](float const l) { return static_cast<double>(l); });\n  utl::sort(levels_sorted, [](auto&& a, auto&& b) { return a > b; });\n  return levels_sorted;\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/map/flex.cc",
    "content": "#include \"motis/endpoints/map/flex_locations.h\"\n\n#include \"utl/to_vec.h\"\n\n#include \"net/bad_request_exception.h\"\n\n#include \"osr/geojson.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace json = boost::json;\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\njson::value to_geometry(n::timetable const& tt, n::flex_area_idx_t const a) {\n  auto const ring_to_json = [](auto&& r) {\n    return utl::transform_to<json::array>(\n        r, [](geo::latlng const& x) { return osr::to_array(x); });\n  };\n\n  auto const get_rings = [&](unsigned const i) {\n    auto rings = json::array{};\n    rings.emplace_back(ring_to_json(tt.flex_area_outers_[a][i]));\n    for (auto const r : tt.flex_area_inners_[a][i]) {\n      rings.emplace_back(ring_to_json(r));\n    }\n    return rings;\n  };\n\n  if (tt.flex_area_outers_[a].size() == 1U) {\n    return {{\"type\", \"Polygon\"}, {\"coordinates\", get_rings(0U)}};\n  } else {\n    auto rings = json::array{};\n    for (auto i = 0U; i != tt.flex_area_outers_[a].size(); ++i) {\n      rings.emplace_back(get_rings(i));\n    }\n    return {{\"type\", \"MultiPolygon\"}, {\"coordinates\", rings}};\n  }\n}\n\nboost::json::value flex_locations::operator()(\n    boost::urls::url_view const& url) const {\n  auto const query = api::stops_params{url.params()};\n  auto const min = parse_location(query.min_);\n  auto const max = parse_location(query.max_);\n\n  utl::verify<net::bad_request_exception>(\n      min.has_value(), \"min not a coordinate: {}\", query.min_);\n  utl::verify<net::bad_request_exception>(\n      max.has_value(), \"max not a coordinate: {}\", query.max_);\n\n  auto features = json::array{};\n  tt_.flex_area_rtree_.search(\n      min->pos_.lnglat_float(), max->pos_.lnglat_float(),\n      [&](auto&&, auto&&, n::flex_area_idx_t const a) {\n        features.emplace_back(json::value{\n            {\"type\", \"Feature\"},\n            {\"id\", tt_.strings_.get(tt_.flex_area_id_[a])},\n            {\"geometry\", to_geometry(tt_, a)},\n            {\"properties\",\n             {{\"stop_name\",\n               tt_.translate(query.language_, tt_.flex_area_name_[a])},\n              {\"stop_desc\",\n               tt_.translate(query.language_, tt_.flex_area_desc_[a])}}}});\n        return true;\n      });\n  loc_rtree_.find({min->pos_, max->pos_}, [&](n::location_idx_t const l) {\n    if (!tt_.location_location_groups_[l].empty()) {\n      features.emplace_back(json::value{\n          {\"type\", \"Feature\"},\n          {\"id\", tags_.id(tt_, l)},\n          {\"geometry\", osr::to_point(osr::point::from_latlng(\n                           tt_.locations_.coordinates_[l]))},\n          {\"properties\",\n           {{\"name\", tt_.translate(query.language_, tt_.locations_.names_[l])},\n            {\"location_groups\",\n             utl::transform_to<json::array>(\n                 tt_.location_location_groups_[l],\n                 [&](n::location_group_idx_t const l) -> json::string {\n                   return {tt_.translate(query.language_,\n                                         tt_.location_group_name_[l])};\n                 })}}}});\n    }\n    return true;\n  });\n\n  return {{\"type\", \"FeatureCollection\"}, {\"features\", std::move(features)}};\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/map/rental.cc",
    "content": "#include \"motis/endpoints/map/rental.h\"\n\n#include <algorithm>\n#include <array>\n#include <cassert>\n#include <set>\n#include <utility>\n#include <vector>\n\n#include \"utl/enumerate.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/overloaded.h\"\n#include \"utl/to_vec.h\"\n\n#include \"geo/box.h\"\n#include \"geo/polyline.h\"\n#include \"geo/polyline_format.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/gbfs/mode.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/place.h\"\n\nnamespace json = boost::json;\n\nnamespace motis::ep {\n\napi::rentals_response rental::operator()(\n    boost::urls::url_view const& url) const {\n  auto const parse_loc = [](std::string_view const sv) {\n    return parse_location(sv);\n  };\n  auto const parse_place_pos = [&](std::string_view const sv) {\n    auto const place = get_place(tt_, tags_, sv);\n    return std::visit(\n        utl::overloaded{[&](osr::location const& l) { return l.pos_; },\n                        [&](tt_location const tt_l) {\n                          return tt_->locations_.coordinates_.at(tt_l.l_);\n                        }},\n        place);\n  };\n\n  auto const query = api::rentals_params{url.params()};\n  auto const min = query.min_.and_then(parse_loc);\n  auto const max = query.max_.and_then(parse_loc);\n  auto const point_pos = query.point_.transform(parse_place_pos);\n  auto const point_radius = query.radius_;\n  auto const filter_bbox = min.has_value() && max.has_value();\n  auto const filter_point = point_pos.has_value() && point_radius.has_value();\n  auto const filter_providers =\n      query.providers_.has_value() && !query.providers_->empty();\n  auto const filter_groups =\n      query.providerGroups_.has_value() && !query.providerGroups_->empty();\n\n  auto gbfs = gbfs_;\n  auto res = api::rentals_response{};\n\n  if (gbfs == nullptr) {\n    return res;\n  }\n\n  auto const restrictions_to_api = [&](gbfs::geofencing_restrictions const& r) {\n    return api::RentalZoneRestrictions{\n        .vehicleTypeIdxs_ = {},\n        .rideStartAllowed_ = r.ride_start_allowed_,\n        .rideEndAllowed_ = r.ride_end_allowed_,\n        .rideThroughAllowed_ = r.ride_through_allowed_,\n        .stationParking_ = r.station_parking_};\n  };\n\n  auto const rule_to_api = [&](gbfs::rule const& r) {\n    return api::RentalZoneRestrictions{\n        .vehicleTypeIdxs_ =\n            utl::to_vec(r.vehicle_type_idxs_,\n                        [&](auto const vti) {\n                          return static_cast<std::int64_t>(to_idx(vti));\n                        }),\n        .rideStartAllowed_ = r.ride_start_allowed_,\n        .rideEndAllowed_ = r.ride_end_allowed_,\n        .rideThroughAllowed_ = r.ride_through_allowed_,\n        .stationParking_ = r.station_parking_};\n  };\n\n  auto const ring_to_api = [&](tg_ring const* ring) {\n    auto enc = geo::polyline_encoder<6>{};\n    auto const np = tg_ring_num_points(ring);\n    for (auto i = 0; i != np; ++i) {\n      auto const pt = tg_ring_point_at(ring, i);\n      enc.push(geo::latlng{pt.y, pt.x});\n    }\n    return api::EncodedPolyline{\n        .points_ = std::move(enc.buf_), .precision_ = 6, .length_ = np};\n  };\n\n  auto const multipoly_to_api = [&](tg_geom* const geom) {\n    assert(tg_geom_typeof(geom) == TG_MULTIPOLYGON);\n    auto mp = api::MultiPolygon{};\n    for (auto i = 0; i != tg_geom_num_polys(geom); ++i) {\n      auto const* poly = tg_geom_poly_at(geom, i);\n      auto polylines = std::vector<api::EncodedPolyline>{};\n      polylines.emplace_back(ring_to_api(tg_poly_exterior(poly)));\n      for (int j = 0; j != tg_poly_num_holes(poly); ++j) {\n        polylines.emplace_back(ring_to_api(tg_poly_hole_at(poly, j)));\n      }\n      mp.push_back(std::move(polylines));\n    }\n    return mp;\n  };\n\n  auto const add_provider = [&](gbfs::gbfs_provider const* provider) {\n    if (!query.withProviders_) {\n      return;\n    }\n    auto form_factors = std::vector<api::RentalFormFactorEnum>{};\n    for (auto const& vt : provider->vehicle_types_) {\n      auto const ff = gbfs::to_api_form_factor(vt.form_factor_);\n      if (utl::find(form_factors, ff) == end(form_factors)) {\n        form_factors.push_back(ff);\n      }\n    }\n    res.providers_.emplace_back(api::RentalProvider{\n        .id_ = provider->id_,\n        .name_ = provider->sys_info_.name_,\n        .groupId_ = provider->group_id_,\n        .operator_ = provider->sys_info_.operator_,\n        .url_ = provider->sys_info_.url_,\n        .purchaseUrl_ = provider->sys_info_.purchase_url_,\n        .color_ = provider->color_,\n        .bbox_ = {provider->bbox_.min_.lng_, provider->bbox_.min_.lat_,\n                  provider->bbox_.max_.lng_, provider->bbox_.max_.lat_},\n        .vehicleTypes_ = utl::to_vec(\n            provider->vehicle_types_,\n            [&](gbfs::vehicle_type const& vt) {\n              return api::RentalVehicleType{\n                  .id_ = vt.id_,\n                  .name_ = vt.name_,\n                  .formFactor_ = gbfs::to_api_form_factor(vt.form_factor_),\n                  .propulsionType_ =\n                      gbfs::to_api_propulsion_type(vt.propulsion_type_),\n                  .returnConstraint_ =\n                      gbfs::to_api_return_constraint(vt.return_constraint_),\n                  .returnConstraintGuessed_ = !vt.known_return_constraint_};\n            }),\n        .formFactors_ = std::move(form_factors),\n        .defaultRestrictions_ =\n            restrictions_to_api(provider->default_restrictions_),\n        .globalGeofencingRules_ =\n            utl::to_vec(provider->geofencing_zones_.global_rules_,\n                        [&](gbfs::rule const& r) { return rule_to_api(r); })});\n  };\n\n  auto const add_provider_group = [&](gbfs::gbfs_group const& group) {\n    auto form_factors = std::vector<api::RentalFormFactorEnum>{};\n    auto color = group.color_;\n    for (auto const& pi : group.providers_) {\n      auto const& provider = gbfs->providers_.at(pi);\n      if (provider == nullptr) {\n        // shouldn't be possible, but just in case...\n        std::cerr << \"[rental api] warning: provider group \" << group.id_\n                  << \" references missing provider idx \" << to_idx(pi) << \"\\n\";\n        continue;\n      }\n      for (auto const& vt : provider->vehicle_types_) {\n        auto const ff = gbfs::to_api_form_factor(vt.form_factor_);\n        if (utl::find(form_factors, ff) == end(form_factors)) {\n          form_factors.push_back(ff);\n        }\n        if (!color && provider->color_) {\n          color = provider->color_;\n        }\n      }\n    }\n    auto provider_ids = std::vector<std::string>{};\n    if (query.withProviders_) {\n      provider_ids.reserve(group.providers_.size());\n      for (auto const& pi : group.providers_) {\n        auto const& provider = gbfs->providers_.at(pi);\n        if (provider == nullptr) {\n          // shouldn't be possible, but just in case...\n          std::cerr << \"[rental api] warning: provider group \" << group.id_\n                    << \" references missing provider idx \" << to_idx(pi)\n                    << \" (providers list)\\n\";\n          continue;\n        }\n        provider_ids.push_back(provider->id_);\n      }\n    }\n\n    res.providerGroups_.emplace_back(\n        api::RentalProviderGroup{.id_ = group.id_,\n                                 .name_ = group.name_,\n                                 .color_ = color,\n                                 .providers_ = std::move(provider_ids),\n                                 .formFactors_ = form_factors});\n  };\n\n  if (!filter_bbox && !filter_point && !filter_providers && !filter_groups) {\n    for (auto const& provider : gbfs->providers_) {\n      if (provider != nullptr) {\n        add_provider(provider.get());\n      }\n    }\n    for (auto const& group : gbfs->groups_ | std::views::values) {\n      add_provider_group(group);\n    }\n    return res;\n  }\n\n  auto bbox = filter_bbox ? geo::box{min->pos_, max->pos_} : geo::box{};\n  auto const in_bbox = [&](geo::latlng const& pos) {\n    return filter_bbox ? bbox.contains(pos) : true;\n  };\n\n  auto const approx_distance_lng_degrees =\n      point_pos ? geo::approx_distance_lng_degrees(*point_pos) : 0.0;\n  auto const point_radius_squared =\n      point_radius ? (*point_radius) * (*point_radius) : 0.0;\n  auto const in_radius = [&](geo::latlng const& pos) {\n    return filter_point ? geo::approx_squared_distance(\n                              *point_pos, pos, approx_distance_lng_degrees) <=\n                              point_radius_squared\n                        : true;\n  };\n\n  auto const include_bbox = [&](geo::box const& b) {\n    if (filter_bbox && !b.overlaps(bbox)) {\n      return false;\n    }\n    if (filter_point) {\n      auto const closest =\n          geo::latlng{std::clamp(point_pos->lat(), b.min_.lat(), b.max_.lat()),\n                      std::clamp(point_pos->lng(), b.min_.lng(), b.max_.lng())};\n      return geo::approx_squared_distance(*point_pos, closest,\n                                          approx_distance_lng_degrees) <=\n             point_radius_squared;\n    }\n    return true;\n  };\n\n  auto providers = hash_set<gbfs::gbfs_provider const*>{};\n  auto provider_groups = hash_set<std::string>{};\n\n  auto check_provider = [&](gbfs_provider_idx_t const pi) {\n    auto const& provider = gbfs->providers_.at(pi);\n    if (provider == nullptr || providers.contains(provider.get())) {\n      return;\n    }\n    if ((filter_providers || filter_groups) &&\n        !((filter_providers && utl::find(*query.providers_, provider->id_) !=\n                                   end(*query.providers_)) ||\n          (filter_groups &&\n           utl::find(*query.providerGroups_, provider->group_id_) !=\n               end(*query.providerGroups_)))) {\n      return;\n    }\n    providers.insert(provider.get());\n    provider_groups.insert(provider->group_id_);\n  };\n\n  if (filter_point) {\n    gbfs->provider_rtree_.in_radius(\n        *point_pos, *point_radius,\n        [&](gbfs_provider_idx_t const pi) { check_provider(pi); });\n    gbfs->provider_zone_rtree_.in_radius(\n        *point_pos, *point_radius,\n        [&](gbfs_provider_idx_t const pi) { check_provider(pi); });\n  } else if (filter_bbox) {\n    gbfs->provider_rtree_.find(\n        bbox, [&](gbfs_provider_idx_t const pi) { check_provider(pi); });\n    gbfs->provider_zone_rtree_.find(\n        bbox, [&](gbfs_provider_idx_t const pi) { check_provider(pi); });\n  } else if (filter_providers || filter_groups) {\n    if (filter_providers) {\n      for (auto const& id : *query.providers_) {\n        if (auto const it = gbfs->provider_by_id_.find(id);\n            it != end(gbfs->provider_by_id_)) {\n          auto const& provider = gbfs->providers_.at(it->second);\n          if (provider != nullptr) {\n            providers.insert(provider.get());\n            provider_groups.insert(provider->group_id_);\n          }\n        }\n      }\n    }\n    if (filter_groups) {\n      for (auto const& id : *query.providerGroups_) {\n        if (auto const it = gbfs->groups_.find(id); it != end(gbfs->groups_)) {\n          provider_groups.insert(it->second.id_);\n          for (auto const pi : it->second.providers_) {\n            auto const& provider = gbfs->providers_.at(pi);\n            if (provider == nullptr) {\n              // shouldn't be possible, but just in case...\n              std::cerr << \"[rental api] warning: provider group \"\n                        << it->second.id_ << \" references missing provider idx \"\n                        << to_idx(pi) << \"\\n\";\n              continue;\n            }\n            providers.insert(provider.get());\n          }\n        }\n      }\n    }\n  }\n\n  for (auto const* provider : providers) {\n    add_provider(provider);\n\n    if (query.withStations_) {\n      for (auto const& st : provider->stations_ | std::views::values) {\n        auto const sbb = st.info_.bounding_box();\n        if (!include_bbox(sbb)) {\n          continue;\n        }\n        auto form_factor_counts =\n            std::array<std::uint64_t,\n                       std::to_underlying(api::RentalFormFactorEnum::OTHER) +\n                           1>{};\n        auto types_available = std::map<std::string, std::uint64_t>{};\n        auto docks_available = std::map<std::string, std::uint64_t>{};\n        auto form_factors = std::set<api::RentalFormFactorEnum>{};\n\n        for (auto const& [vti, count] : st.status_.vehicle_types_available_) {\n          if (vti == gbfs::vehicle_type_idx_t::invalid() ||\n              cista::to_idx(vti) >= provider->vehicle_types_.size()) {\n            continue;\n          }\n          auto const& vt = provider->vehicle_types_.at(vti);\n          auto const api_ff = gbfs::to_api_form_factor(vt.form_factor_);\n          form_factor_counts[static_cast<std::size_t>(\n              std::to_underlying(api_ff))] += count;\n          types_available[vt.id_] = count;\n          form_factors.insert(api_ff);\n        }\n        for (auto const& [vti, count] : st.status_.vehicle_docks_available_) {\n          if (vti == gbfs::vehicle_type_idx_t::invalid() ||\n              cista::to_idx(vti) >= provider->vehicle_types_.size()) {\n            continue;\n          }\n          auto const& vt = provider->vehicle_types_.at(vti);\n          auto const api_ff = gbfs::to_api_form_factor(vt.form_factor_);\n          form_factor_counts[static_cast<std::size_t>(\n              std::to_underlying(api_ff))] += count;\n          docks_available[vt.id_] = count;\n          form_factors.insert(api_ff);\n        }\n\n        if (form_factors.empty()) {\n          for (auto const& vt : provider->vehicle_types_) {\n            form_factors.insert(gbfs::to_api_form_factor(vt.form_factor_));\n          }\n        }\n\n        auto sorted_form_factors = utl::to_vec(form_factors);\n        utl::sort(sorted_form_factors, [&](auto const a, auto const b) {\n          return form_factor_counts[static_cast<std::size_t>(\n                     std::to_underlying(a))] >\n                 form_factor_counts[static_cast<std::size_t>(\n                     std::to_underlying(b))];\n        });\n\n        res.stations_.emplace_back(api::RentalStation{\n            .id_ = st.info_.id_,\n            .providerId_ = provider->id_,\n            .providerGroupId_ = provider->group_id_,\n            .name_ = st.info_.name_,\n            .lat_ = st.info_.pos_.lat_,\n            .lon_ = st.info_.pos_.lng_,\n            .address_ = st.info_.address_,\n            .crossStreet_ = st.info_.cross_street_,\n            .rentalUriAndroid_ = st.info_.rental_uris_.android_,\n            .rentalUriIOS_ = st.info_.rental_uris_.ios_,\n            .rentalUriWeb_ = st.info_.rental_uris_.web_,\n            .isRenting_ = st.status_.is_renting_,\n            .isReturning_ = st.status_.is_returning_,\n            .numVehiclesAvailable_ = st.status_.num_vehicles_available_,\n            .formFactors_ = sorted_form_factors,\n            .vehicleTypesAvailable_ = std::move(types_available),\n            .vehicleDocksAvailable_ = std::move(docks_available),\n            .stationArea_ = st.info_.station_area_ != nullptr\n                                ? std::optional{multipoly_to_api(\n                                      st.info_.station_area_.get())}\n                                : std::nullopt,\n            .bbox_ = {sbb.min_.lng_, sbb.min_.lat_, sbb.max_.lng_,\n                      sbb.max_.lat_}});\n      }\n    }\n\n    if (query.withVehicles_) {\n      auto const add_vehicle = [&](gbfs::vehicle_status const& vs,\n                                   gbfs::vehicle_type const& vt) {\n        res.vehicles_.emplace_back(api::RentalVehicle{\n            .id_ = vs.id_,\n            .providerId_ = provider->id_,\n            .providerGroupId_ = provider->group_id_,\n            .typeId_ = vt.id_,\n            .lat_ = vs.pos_.lat_,\n            .lon_ = vs.pos_.lng_,\n            .formFactor_ = gbfs::to_api_form_factor(vt.form_factor_),\n            .propulsionType_ =\n                gbfs::to_api_propulsion_type(vt.propulsion_type_),\n            .returnConstraint_ =\n                gbfs::to_api_return_constraint(vt.return_constraint_),\n            .stationId_ = vs.station_id_,\n            .homeStationId_ = vs.home_station_id_,\n            .isReserved_ = vs.is_reserved_,\n            .isDisabled_ = vs.is_disabled_,\n            .rentalUriAndroid_ = vs.rental_uris_.android_,\n            .rentalUriIOS_ = vs.rental_uris_.ios_,\n            .rentalUriWeb_ = vs.rental_uris_.web_,\n        });\n      };\n      auto const fallback_vt =\n          gbfs::vehicle_type{.form_factor_ = gbfs::vehicle_form_factor::kOther};\n      for (auto const& vs : provider->vehicle_status_) {\n        if (in_bbox(vs.pos_) && in_radius(vs.pos_)) {\n          if (vs.vehicle_type_idx_ != gbfs::vehicle_type_idx_t::invalid() &&\n              cista::to_idx(vs.vehicle_type_idx_) <\n                  provider->vehicle_types_.size()) {\n            auto const& vt = provider->vehicle_types_.at(vs.vehicle_type_idx_);\n            add_vehicle(vs, vt);\n          } else {\n            add_vehicle(vs, fallback_vt);\n          }\n        }\n      }\n    }\n\n    if (query.withZones_) {\n      auto const n_zones =\n          static_cast<std::int64_t>(provider->geofencing_zones_.zones_.size());\n      for (auto const [order, zone] :\n           utl::enumerate(provider->geofencing_zones_.zones_)) {\n        auto const zbb = zone.bounding_box();\n        if (!include_bbox(zbb)) {\n          continue;\n        }\n        res.zones_.emplace_back(api::RentalZone{\n            .providerId_ = provider->id_,\n            .providerGroupId_ = provider->group_id_,\n            .name_ = zone.name_,\n            .z_ = n_zones - static_cast<std::int64_t>(order),\n            .bbox_ = {zbb.min_.lng_, zbb.min_.lat_, zbb.max_.lng_,\n                      zbb.max_.lat_},\n            .area_ = multipoly_to_api(zone.geom_.get()),\n            .rules_ = utl::to_vec(\n                zone.rules_,\n                [&](gbfs::rule const& r) { return rule_to_api(r); }),\n        });\n      }\n    }\n  }\n\n  for (auto const& group_id : provider_groups) {\n    add_provider_group(gbfs->groups_.at(group_id));\n  }\n\n  return res;\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/map/route_details.cc",
    "content": "#include \"motis/endpoints/map/route_details.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/data.h\"\n#include \"motis/fwd.h\"\n#include \"motis/railviz.h\"\n#include \"motis/server.h\"\n\nnamespace motis::ep {\n\napi::routeDetails_response route_details::operator()(\n    boost::urls::url_view const& url) const {\n  auto const api_version = get_api_version(url);\n  auto const rt = rt_;\n  return get_route_details(tags_, tt_, rt->rtt_.get(), shapes_, w_, pl_,\n                           matches_, ae_, tz_, *static_.impl_,\n                           *rt->railviz_rt_->impl_,\n                           api::routeDetails_params{url.params()}, api_version);\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/map/routes.cc",
    "content": "#include \"motis/endpoints/map/routes.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/data.h\"\n#include \"motis/fwd.h\"\n#include \"motis/railviz.h\"\n#include \"motis/server.h\"\n\nnamespace motis::ep {\n\napi::routes_response routes::operator()(\n    boost::urls::url_view const& url) const {\n  auto const api_version = get_api_version(url);\n  auto const rt = rt_;\n  return get_routes(tags_, tt_, rt->rtt_.get(), shapes_, w_, pl_, matches_, ae_,\n                    tz_, *static_.impl_, *rt->railviz_rt_->impl_,\n                    api::routes_params{url.params()}, api_version);\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/map/shapes_debug.cc",
    "content": "#include \"motis/endpoints/map/shapes_debug.h\"\n\n#include <charconv>\n#include <set>\n#include <string>\n#include <string_view>\n#include <tuple>\n\n#include \"boost/json.hpp\"\n\n#include \"fmt/format.h\"\n\n#include \"utl/to_vec.h\"\n#include \"utl/verify.h\"\n\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/timetable.h\"\n#include \"nigiri/types.h\"\n\n#include \"osr/routing/map_matching_debug.h\"\n\n#include \"motis/route_shapes.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace motis::ep {\n\nnamespace {\n\nstd::uint64_t parse_route_idx(std::string_view const path) {\n  auto const slash = path.find_last_of('/');\n  auto const idx_str =\n      slash == std::string_view::npos ? path : path.substr(slash + 1U);\n  utl::verify(!idx_str.empty(), \"missing route index\");\n\n  auto route_idx = std::uint64_t{};\n  auto const* begin = idx_str.data();\n  auto const* end = begin + idx_str.size();\n  auto const [ptr, ec] = std::from_chars(begin, end, route_idx);\n  utl::verify(ec == std::errc{} && ptr == end, \"invalid route index '{}'\",\n              idx_str);\n  return route_idx;\n}\n\nboost::json::object build_caller_data(nigiri::timetable const& tt,\n                                      tag_lookup const& tags,\n                                      nigiri::route_idx_t const route,\n                                      std::uint64_t const route_idx,\n                                      nigiri::clasz const clasz) {\n  auto const lang = nigiri::lang_t{};\n  auto const stop_count =\n      static_cast<nigiri::stop_idx_t>(tt.route_location_seq_[route].size());\n\n  auto route_infos =\n      std::set<std::tuple<std::string, std::string, std::string>>{};\n  auto trip_ids = boost::json::array{};\n\n  for (auto const transport_idx : tt.route_transport_ranges_[route]) {\n    auto const fr = nigiri::rt::frun{\n        tt, nullptr,\n        nigiri::rt::run{\n            .t_ = nigiri::transport{transport_idx, nigiri::day_idx_t{0}},\n            .stop_range_ = nigiri::interval{nigiri::stop_idx_t{0U}, stop_count},\n            .rt_ = nigiri::rt_transport_idx_t::invalid()}};\n\n    auto const first = fr[nigiri::stop_idx_t{0U}];\n    route_infos.emplace(\n        std::string{first.get_route_id(nigiri::event_type::kDep)},\n        std::string{first.route_short_name(nigiri::event_type::kDep, lang)},\n        std::string{first.route_long_name(nigiri::event_type::kDep, lang)});\n\n    trip_ids.emplace_back(\n        boost::json::string{tags.id(tt, first, nigiri::event_type::kDep)});\n  }\n\n  return boost::json::object{\n      {\"route_index\", route_idx},\n      {\"route_clasz\", to_str(clasz)},\n      {\"timetable_routes\", utl::transform_to<boost::json::array>(\n                               route_infos,\n                               [](auto const& info) {\n                                 auto const& [id, short_name, long_name] = info;\n                                 return boost::json::object{\n                                     {\"id\", id},\n                                     {\"short_name\", short_name},\n                                     {\"long_name\", long_name}};\n                               })},\n      {\"trip_ids\", std::move(trip_ids)}};\n}\n\n}  // namespace\n\nnet::reply shapes_debug::operator()(net::route_request const& req, bool) const {\n  utl::verify(c_.shapes_debug_api_enabled(),\n              \"route shapes debug API is disabled\");\n  utl::verify(\n      w_ != nullptr && l_ != nullptr && tt_ != nullptr && tags_ != nullptr,\n      \"data not loaded\");\n\n  auto const url = boost::url_view{req.target()};\n  auto const route_idx = parse_route_idx(url.path());\n  utl::verify(route_idx < tt_->n_routes(), \"invalid route index {} (max={})\",\n              route_idx, tt_->n_routes() == 0U ? 0U : tt_->n_routes() - 1U);\n\n  auto const route =\n      nigiri::route_idx_t{static_cast<nigiri::route_idx_t::value_t>(route_idx)};\n  auto const clasz = tt_->route_clasz_[route];\n\n  auto debug_json = route_shape_debug(*w_, *l_, *tt_, route);\n  debug_json[\"caller\"] =\n      build_caller_data(*tt_, *tags_, route, route_idx, clasz);\n  auto payload = osr::gzip_json(debug_json);\n  auto const filename =\n      fmt::format(\"r_{}_{}.json.gz\", route_idx, to_str(clasz));\n\n  auto res = net::web_server::string_res_t{boost::beast::http::status::ok,\n                                           req.version()};\n  res.insert(boost::beast::http::field::content_type, \"application/gzip\");\n  res.insert(boost::beast::http::field::content_disposition,\n             fmt::format(\"attachment; filename=\\\"{}\\\"\", filename));\n  res.insert(boost::beast::http::field::access_control_expose_headers,\n             \"content-disposition\");\n  res.body() = std::move(payload);\n  res.keep_alive(req.keep_alive());\n  return res;\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/map/stops.cc",
    "content": "#include \"motis/endpoints/map/stops.h\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/too_many_exception.h\"\n\n#include \"osr/geojson.h\"\n\n#include \"motis/journey_to_response.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace json = boost::json;\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\napi::stops_response stops::operator()(boost::urls::url_view const& url) const {\n  auto const query = api::stops_params{url.params()};\n  auto const min = parse_location(query.min_);\n  auto const max = parse_location(query.max_);\n  utl::verify<net::bad_request_exception>(\n      min.has_value(), \"min not a coordinate: {}\", query.min_);\n  utl::verify<net::bad_request_exception>(\n      max.has_value(), \"max not a coordinate: {}request_exception\", query.max_);\n  auto res = api::stops_response{};\n\n  auto const max_results = config_.get_limits().stops_max_results_;\n  loc_rtree_.find({min->pos_, max->pos_}, [&](n::location_idx_t const l) {\n    utl::verify<net::too_many_exception>(res.size() < max_results,\n                                         \"too many items\");\n    res.emplace_back(to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_,\n                              query.language_, tt_location{l}));\n  });\n  return res;\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/map/trips.cc",
    "content": "#include \"motis/endpoints/map/trips.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/data.h\"\n#include \"motis/fwd.h\"\n#include \"motis/railviz.h\"\n#include \"motis/server.h\"\n\nnamespace motis::ep {\n\napi::trips_response trips::operator()(boost::urls::url_view const& url) const {\n  auto const api_version = get_api_version(url);\n  auto const rt = std::atomic_load(&rt_);\n  return get_trains(tags_, tt_, rt->rtt_.get(), shapes_, w_, pl_, matches_, ae_,\n                    tz_, *static_.impl_, *rt->railviz_rt_->impl_,\n                    api::trips_params{url.params()}, api_version);\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/matches.cc",
    "content": "#include \"motis/endpoints/matches.h\"\n\n#include \"net/too_many_exception.h\"\n\n#include \"osr/geojson.h\"\n\n#include \"motis/location_routes.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace json = boost::json;\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\nconstexpr auto const kLimit = 2048;\n\nstd::string get_names(osr::platforms const& pl, osr::platform_idx_t const x) {\n  auto ss = std::stringstream{};\n  for (auto const y : pl.platform_names_[x]) {\n    ss << y.view() << \", \";\n  }\n  return ss.str();\n}\n\njson::value matches::operator()(json::value const& query) const {\n  auto const& q = query.as_array();\n\n  auto const min = geo::latlng{q[1].as_double(), q[0].as_double()};\n  auto const max = geo::latlng{q[3].as_double(), q[2].as_double()};\n\n  auto matches = json::array{};\n\n  pl_.find(min, max, [&](osr::platform_idx_t const p) {\n    utl::verify<net::too_many_exception>(matches.size() < kLimit,\n                                         \"too many items\");\n\n    auto const center = get_platform_center(pl_, w_, p);\n    if (!center.has_value()) {\n      return;\n    }\n    auto platform_data =\n        json::value{{\"type\", \"platform\"},\n                    {\"level\", pl_.get_level(w_, p).to_float()},\n                    {\"platform_names\", fmt::format(\"{}\", get_names(pl_, p))}}\n            .as_object();\n    std::visit(utl::overloaded{[&](osr::way_idx_t x) {\n                                 platform_data.emplace(\n                                     \"osm_way_id\", to_idx(w_.way_osm_idx_[x]));\n                               },\n                               [&](osr::node_idx_t x) {\n                                 platform_data.emplace(\n                                     \"osm_node_id\", to_idx(w_.node_to_osm_[x]));\n                               }},\n               osr::to_ref(pl_.platform_ref_[p][0]));\n\n    matches.emplace_back(json::value{\n        {\"type\", \"Feature\"},\n        {\"properties\", platform_data},\n        {\"geometry\", osr::to_point(osr::point::from_latlng(*center))}});\n  });\n\n  loc_rtree_.find({min, max}, [&](n::location_idx_t const l) {\n    utl::verify<net::too_many_exception>(matches.size() < kLimit,\n                                         \"too many items\");\n\n    auto const pos = tt_.locations_.coordinates_[l];\n    auto const match = get_match(tt_, pl_, w_, l);\n    auto props =\n        json::value{\n            {\"name\", tt_.get_default_translation(tt_.locations_.names_[l])},\n            {\"id\", tags_.id(tt_, l)},\n            {\"src\", to_idx(tt_.locations_.src_[l])},\n            {\"platform_codes\",\n             tt_.get_default_translation(tt_.locations_.platform_codes_[l])},\n            {\"type\", \"location\"},\n            {\"trips\", fmt::format(\"{}\", get_location_routes(tt_, l))}}\n            .as_object();\n    if (match == osr::platform_idx_t::invalid()) {\n      props.emplace(\"level\", \"-\");\n    } else {\n      std::visit(utl::overloaded{\n                     [&](osr::way_idx_t x) {\n                       props.emplace(\"osm_way_id\", to_idx(w_.way_osm_idx_[x]));\n                       props.emplace(\n                           \"level\",\n                           w_.r_->way_properties_[x].from_level().to_float());\n                     },\n                     [&](osr::node_idx_t x) {\n                       props.emplace(\"osm_node_id\", to_idx(w_.node_to_osm_[x]));\n                       props.emplace(\n                           \"level\",\n                           w_.r_->node_properties_[x].from_level().to_float());\n                     }},\n                 osr::to_ref(pl_.platform_ref_[match][0]));\n    }\n    matches.emplace_back(\n        json::value{{\"type\", \"Feature\"},\n                    {\"properties\", props},\n                    {\"geometry\", osr::to_point(osr::point::from_latlng(pos))}});\n\n    if (match == osr::platform_idx_t::invalid()) {\n      return;\n    }\n\n    props.emplace(\"platform_names\", fmt::format(\"{}\", get_names(pl_, match)));\n\n    auto const center = get_platform_center(pl_, w_, match);\n    if (!center.has_value()) {\n      return;\n    }\n\n    props.insert_or_assign(\"type\", \"match\");\n    matches.emplace_back(json::value{\n        {\"type\", \"Feature\"},\n        {\"properties\", props},\n        {\"geometry\", osr::to_line_string({osr::point::from_latlng(*center),\n                                          osr::point::from_latlng(pos)})}});\n  });\n  return json::value{{\"type\", \"FeatureCollection\"}, {\"features\", matches}};\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/metrics.cc",
    "content": "#include \"motis/endpoints/metrics.h\"\n\n#include <chrono>\n#include <functional>\n#include <iostream>\n\n#include \"prometheus/registry.h\"\n#include \"prometheus/text_serializer.h\"\n\n#include \"utl/enumerate.h\"\n\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/timetable.h\"\n#include \"nigiri/timetable_metrics.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/data.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\nvoid update_all_runs_metrics(nigiri::timetable const& tt,\n                             nigiri::rt_timetable const* rtt,\n                             tag_lookup const& tags,\n                             metrics_registry& metrics) {\n  auto const start_time =\n      std::max(std::chrono::time_point_cast<n::unixtime_t::duration>(\n                   std::chrono::system_clock::now()),\n               tt.external_interval().from_);\n  auto const end_time = std::min(\n      std::chrono::time_point_cast<n::unixtime_t::duration>(\n          start_time +\n          std::chrono::duration_cast<n::duration_t>(std::chrono::minutes{3})),\n      tt.external_interval().to_);\n  auto const time_interval = n::interval{start_time, end_time};\n\n  auto metric_by_agency =\n      std::vector<std::pair<std::reference_wrapper<prometheus::Gauge>,\n                            std::reference_wrapper<prometheus::Gauge>>>{};\n  metric_by_agency.reserve(tt.n_agencies());\n  for (auto i = nigiri::provider_idx_t{0}; i < tt.n_agencies(); ++i) {\n    auto const& p = tt.providers_[i];\n    auto const labels = prometheus::Labels{\n        {\"tag\", std::string{tags.get_tag(p.src_)}},\n        {\"agency_name\", std::string{tt.get_default_translation(p.name_)}},\n        {\"agency_id\", std::string{tt.strings_.get(p.id_)}}};\n    auto& sched = metrics.current_trips_running_scheduled_count_.Add(labels);\n    auto& real =\n        metrics.current_trips_running_scheduled_with_realtime_count_.Add(\n            labels);\n    sched.Set(0);\n    real.Set(0);\n    metric_by_agency.emplace_back(std::ref(sched), std::ref(real));\n  }\n\n  if (rtt != nullptr) {\n    for (auto rt_t = nigiri::rt_transport_idx_t{0};\n         rt_t < rtt->n_rt_transports(); ++rt_t) {\n      auto const fr = n::rt::frun::from_rt(tt, rtt, rt_t);\n      if (!fr.is_scheduled()) {\n        continue;\n      }\n      auto const active = n::interval{\n          fr[0].time(n::event_type::kDep),\n          fr[static_cast<n::stop_idx_t>(fr.stop_range_.size() - 1)].time(\n              n::event_type::kArr) +\n              n::unixtime_t::duration{1}};\n      if (active.overlaps(time_interval)) {\n        auto const provider_idx = fr[0].get_provider_idx(n::event_type::kDep);\n        if (provider_idx != n::provider_idx_t::invalid()) {\n          metric_by_agency.at(provider_idx.v_).first.get().Increment();\n          metric_by_agency.at(provider_idx.v_).second.get().Increment();\n        }\n      }\n    }\n  }\n\n  auto const [start_day, _] = tt.day_idx_mam(time_interval.from_);\n  auto const [end_day, _1] = tt.day_idx_mam(time_interval.to_);\n\n  for (auto r = nigiri::route_idx_t{0}; r < tt.n_routes(); ++r) {\n    auto const is_active = [&](n::transport const t) -> bool {\n      return (rtt == nullptr\n                  ? tt.bitfields_[tt.transport_traffic_days_[t.t_idx_]]\n                  : rtt->bitfields_[rtt->transport_traffic_days_[t.t_idx_]])\n          .test(to_idx(t.day_));\n    };\n\n    auto const seq = tt.route_location_seq_[r];\n    auto const from = n::stop_idx_t{0U};\n    auto const to = static_cast<n::stop_idx_t>(seq.size() - 1);\n    auto const arr_times = tt.event_times_at_stop(r, to, n::event_type::kArr);\n\n    for (auto const [i, t_idx] :\n         utl::enumerate(tt.route_transport_ranges_[r])) {\n      auto const day_offset =\n          static_cast<n::day_idx_t::value_t>(arr_times[i].days());\n      for (auto day = start_day - day_offset; day <= end_day; ++day) {\n        auto const t = n::transport{t_idx, day};\n        if (is_active(t) &&\n            time_interval.overlaps({tt.event_time(t, from, n::event_type::kDep),\n                                    tt.event_time(t, to, n::event_type::kArr) +\n                                        n::unixtime_t::duration{1}})) {\n          auto fr = n::rt::frun::from_t(tt, nullptr, t);\n          auto const provider_idx = fr[0].get_provider_idx(n::event_type::kDep);\n          if (provider_idx != n::provider_idx_t::invalid()) {\n            metric_by_agency.at(provider_idx.v_).first.get().Increment();\n          }\n        }\n      }\n    }\n  }\n\n  if (metrics.timetable_first_day_timestamp_.Collect().empty()) {\n    auto const m = get_metrics(tt);\n    constexpr auto const kEndOfDay =\n        std::chrono::days{1} - std::chrono::seconds{1};\n    auto const from = std::chrono::duration_cast<std::chrono::seconds>(\n        tt.internal_interval().from_.time_since_epoch());\n    for (auto src = n::source_idx_t{0U}; src != tt.n_sources(); ++src) {\n      auto const& fm = m.feeds_[src];\n      auto const labels =\n          prometheus::Labels{{\"tag\", std::string{tags.get_tag(src)}}};\n      metrics.timetable_first_day_timestamp_.Add(\n          labels, static_cast<double>((from + date::days{fm.first_}).count()));\n      metrics.timetable_last_day_timestamp_.Add(\n          labels, static_cast<double>(\n                      (from + date::days{fm.last_} + kEndOfDay).count()));\n      metrics.timetable_locations_count_.Add(labels, fm.locations_);\n      metrics.timetable_trips_count_.Add(labels, fm.trips_);\n      metrics.timetable_transports_x_days_count_.Add(\n          labels, static_cast<double>(fm.transport_days_));\n    }\n  }\n}\n\nnet::reply metrics::operator()(net::route_request const& req, bool) const {\n  utl::verify(metrics_ != nullptr && tt_ != nullptr && tags_ != nullptr,\n              \"no metrics initialized\");\n  auto const rt = std::atomic_load(&rt_);\n  update_all_runs_metrics(*tt_, rt->rtt_.get(), *tags_, *metrics_);\n  metrics_->total_trips_with_realtime_count_.Set(\n      static_cast<double>(rt->rtt_->rt_transport_src_.size()));\n  auto res = net::web_server::string_res_t{boost::beast::http::status::ok,\n                                           req.version()};\n  res.insert(boost::beast::http::field::content_type,\n             \"text/plain; version=0.0.4\");\n  set_response_body(\n      res, req,\n      prometheus::TextSerializer{}.Serialize(metrics_->registry_.Collect()));\n  res.keep_alive(req.keep_alive());\n  return res;\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/ojp.cc",
    "content": "#include \"motis/endpoints/ojp.h\"\n\n#include \"pugixml.hpp\"\n\n#include <unordered_set>\n\n#include \"fmt/format.h\"\n\n#include \"date/date.h\"\n\n#include \"geo/polyline_format.h\"\n\n#include \"net/bad_request_exception.h\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"motis/adr_extend_tt.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/clasz_to_mode.h\"\n\nnamespace n = nigiri;\nnamespace sr = std::ranges;\nusing namespace std::string_view_literals;\n\nnamespace motis::ep {\n\ntemplate <typename T>\nT* maybe_ref(T& x) {\n  return &x;\n}\n\ntemplate <typename T>\nT* maybe_ref(T* x) {\n  return x;\n}\n\ntemplate <typename T>\nT& maybe_deref(T& x) {\n  return x;\n}\n\ntemplate <typename T>\nT& maybe_deref(T* x) {\n  utl::verify(x != nullptr, \"not set: {}\", cista::type_str<T>());\n  return *x;\n}\n\nstatic auto response_id = std::atomic_size_t{0U};\n\nstruct transport_mode {\n  std::string_view transport_mode_;\n  std::string_view transport_submode_type_;\n  std::string_view transport_submode_name_;\n};\n\ntransport_mode get_transport_mode(std::int64_t const route_type) {\n  switch (route_type) {\n    case 0: return {\"tram\", \"TramSubmode\", \"\"};\n    case 1: return {\"metro\", \"MetroSubmode\", \"\"};\n    case 2: return {\"rail\", \"RailSubmode\", \"\"};\n    case 3: return {\"bus\", \"BusSubmode\", \"\"};\n    case 4: return {\"ferry\", \"\", \"\"};\n    case 5: return {\"cableway\", \"TelecabinSubmode\", \"cableCar\"};\n    case 6: return {\"telecabin\", \"TelecabinSubmode\", \"telecabin\"};\n    case 7: return {\"funicular\", \"\", \"\"};\n    case 11: return {\"trolleyBus\", \"\", \"\"};\n    case 12: return {\"rail\", \"RailSubmode\", \"monorail\"};\n\n    case 100: return {\"rail\", \"RailSubmode\", \"\"};\n    case 101: return {\"rail\", \"RailSubmode\", \"highSpeedRail\"};\n    case 102: return {\"rail\", \"RailSubmode\", \"longDistance\"};\n    case 103: return {\"rail\", \"RailSubmode\", \"interregionalRail\"};\n    case 104: return {\"rail\", \"RailSubmode\", \"carTransportRailService\"};\n    case 105: return {\"rail\", \"RailSubmode\", \"nightRail\"};\n    case 106: return {\"rail\", \"RailSubmode\", \"regionalRail\"};\n    case 107: return {\"rail\", \"RailSubmode\", \"touristRailway\"};\n    case 108: return {\"rail\", \"RailSubmode\", \"railShuttle\"};\n    case 109: return {\"rail\", \"RailSubmode\", \"suburbanRailway\"};\n    case 110: return {\"rail\", \"RailSubmode\", \"replacementRailService\"};\n    case 111: return {\"rail\", \"RailSubmode\", \"specialTrain\"};\n\n    case 200: return {\"coach\", \"CoachSubmode\", \"\"};\n    case 201: return {\"coach\", \"CoachSubmode\", \"internationalCoach\"};\n    case 202: return {\"coach\", \"CoachSubmode\", \"nationalCoach\"};\n    case 203: return {\"coach\", \"CoachSubmode\", \"shuttleCoach\"};\n    case 204: return {\"coach\", \"CoachSubmode\", \"regionalCoach\"};\n    case 205: return {\"coach\", \"CoachSubmode\", \"specialCoach\"};\n    case 206: return {\"coach\", \"CoachSubmode\", \"sightseeingCoach\"};\n    case 207: return {\"coach\", \"CoachSubmode\", \"touristCoach\"};\n    case 208: return {\"coach\", \"CoachSubmode\", \"commuterCoach\"};\n\n    case 400: return {\"metro\", \"MetroSubmode\", \"urbanRailway\"};\n    case 401: return {\"metro\", \"MetroSubmode\", \"metro\"};\n    case 402: return {\"metro\", \"MetroSubmode\", \"tube\"};\n\n    case 700: return {\"bus\", \"BusSubmode\", \"\"};\n    case 701: return {\"bus\", \"BusSubmode\", \"regionalBus\"};\n    case 702: return {\"bus\", \"BusSubmode\", \"expressBus\"};\n    case 704: return {\"bus\", \"BusSubmode\", \"localBus\"};\n    case 709: return {\"bus\", \"BusSubmode\", \"mobilityBusForRegisteredDisabled\"};\n    case 710: return {\"bus\", \"BusSubmode\", \"sightseeingBus\"};\n    case 711: return {\"bus\", \"BusSubmode\", \"shuttleBus\"};\n    case 712: return {\"bus\", \"BusSubmode\", \"schoolBus\"};\n    case 713: return {\"bus\", \"BusSubmode\", \"schoolAndPublicServiceBus\"};\n    case 714: return {\"bus\", \"BusSubmode\", \"railReplacementBus\"};\n    case 715: return {\"bus\", \"BusSubmode\", \"demandAndResponseBus\"};\n\n    case 900: return {\"tram\", \"TramSubmode\", \"\"};\n    case 901: return {\"tram\", \"TramSubmode\", \"cityTram\"};\n    case 902: return {\"tram\", \"TramSubmode\", \"localTram\"};\n    case 903: return {\"tram\", \"TramSubmode\", \"regionalTram\"};\n    case 904: return {\"tram\", \"TramSubmode\", \"sightseeingTram\"};\n    case 905: return {\"tram\", \"TramSubmode\", \"shuttleTram\"};\n\n    case 1000: return {\"water\", \"\", \"\"};\n    case 1100: return {\"air\", \"\", \"\"};\n\n    case 1300: return {\"telecabin\", \"TelecabinSubmode\", \"\"};\n    case 1301: return {\"telecabin\", \"TelecabinSubmode\", \"telecabin\"};\n    case 1302: return {\"telecabin\", \"TelecabinSubmode\", \"cableCar\"};\n    case 1303: return {\"lift\", \"\", \"\"};\n    case 1304: return {\"telecabin\", \"TelecabinSubmode\", \"chairLift\"};\n    case 1305: return {\"telecabin\", \"TelecabinSubmode\", \"dragLift\"};\n    case 1307: return {\"telecabin\", \"TelecabinSubmode\", \"lift\"};\n\n    case 1400: return {\"funicular\", \"FunicularSubmode\", \"undefinedFunicular\"};\n\n    case 1500: return {\"taxi\", \"TaxiSubmode\", \"\"};\n    case 1501: return {\"taxi\", \"TaxiSubmode\", \"communalTaxi\"};\n    case 1502: return {\"taxi\", \"TaxiSubmode\", \"waterTaxi\"};\n    case 1503: return {\"taxi\", \"TaxiSubmode\", \"railTaxi\"};\n    case 1504: return {\"taxi\", \"TaxiSubmode\", \"bikeTaxi\"};\n    case 1507: return {\"taxi\", \"TaxiSubmode\", \"allTaxiServices\"};\n\n    case 1700:\n    default: return {\"selfDrive\", \"\", \"\"};\n  }\n}\n\ntransport_mode to_pt_mode(api::ModeEnum mode) {\n  using api::ModeEnum;\n  switch (mode) {\n    case ModeEnum::AIRPLANE: return {\"air\", \"\", \"\"};\n    case ModeEnum::HIGHSPEED_RAIL:\n      return {\"rail\", \"RailSubmode\", \"highSpeedRail\"};\n    case ModeEnum::LONG_DISTANCE:\n      return {\"rail\", \"RailSubmode\", \"longDistance\"};\n    case ModeEnum::COACH: return {\"coach\", \"\", \"\"};\n    case ModeEnum::NIGHT_RAIL: return {\"rail\", \"RailSubmode\", \"nightRail\"};\n    case ModeEnum::REGIONAL_FAST_RAIL:\n    case ModeEnum::REGIONAL_RAIL:\n      return {\"rail\", \"RailSubmode\", \"regionalRail\"};\n    case ModeEnum::SUBURBAN: return {\"rail\", \"RailSubmode\", \"suburbanRailway\"};\n    case ModeEnum::SUBWAY: return {\"metro\", \"MetroSubmode\", \"tube\"};\n    case ModeEnum::TRAM: return {\"tram\", \"\", \"\"};\n    case ModeEnum::BUS: return {\"bus\", \"\", \"\"};\n    case ModeEnum::FERRY: return {\"water\", \"\", \"\"};\n    case ModeEnum::ODM: return {\"bus\", \"BusSubmode\", \"demandAndResponseBus\"};\n    case ModeEnum::FUNICULAR: return {\"funicular\", \"\", \"\"};\n    case ModeEnum::AERIAL_LIFT: return {\"telecabin\", \"\", \"\"};\n    case ModeEnum::OTHER:\n    default: return {\"\", \"\", \"\"};\n  }\n}\n\ntemplate <typename T>\nvoid append(std::string_view lang,\n            pugi::xml_node node,\n            std::string_view name,\n            T const& value) {\n  auto text = node.append_child(name).append_child(\"Text\");\n  text.append_attribute(\"xml:lang\").set_value(lang);\n  text.text().set(value);\n}\n\ntemplate <typename T>\nvoid append(std::string_view lang,\n            pugi::xml_node node,\n            std::string_view name,\n            std::optional<T> const& value) {\n  if (value.has_value()) {\n    append(lang, node, name, *value);\n  }\n}\n\ntemplate <typename T>\nvoid append(pugi::xml_node node, std::string_view name, T const& value) {\n  node.append_child(name).text().set(value);\n}\n\ntemplate <typename T>\nvoid append(pugi::xml_node node,\n            std::string_view name,\n            std::optional<T> const& value) {\n  if (value.has_value()) {\n    if constexpr (std::is_same_v<std::string, T>) {\n      node.append_child(name).text().set(value->data());\n    } else {\n      node.append_child(name).text().set(*value);\n    }\n  }\n}\n\nvoid append_stop_ref(pugi::xml_node node, api::Place const& p) {\n  append(node, p.parentId_ ? \"siri:StopPointRef\" : \"siri:StopPlaceRef\",\n         p.stopId_);\n}\n\nvoid append_position(pugi::xml_node node,\n                     geo::latlng const& pos,\n                     std::string_view name = \"Position\");\n\nvoid append_place_ref_or_geo(pugi::xml_node node, api::Place const& p) {\n  if (p.stopId_.has_value()) {\n    append_stop_ref(node, p);\n  } else {\n    append_position(node, {p.lat_, p.lon_}, \"GeoPosition\");\n  }\n}\n\nvoid append_position(pugi::xml_node node,\n                     geo::latlng const& pos,\n                     std::string_view name) {\n  auto geo = node.append_child(name);\n  geo.append_child(\"siri:Longitude\").text().set(pos.lng_, 6);\n  geo.append_child(\"siri:Latitude\").text().set(pos.lat_, 7);\n}\n\nstd::string_view get_place_ref(pugi::xml_node ref) {\n  if (auto a = ref.child(\"StopPlaceRef\")) {\n    return a.text().as_string();\n  }\n  if (auto b = ref.child(\"siri:StopPlaceRef\")) {\n    return b.text().as_string();\n  }\n  if (auto c = ref.child(\"StopPointRef\")) {\n    return c.text().as_string();\n  }\n  if (auto d = ref.child(\"siri:StopPointRef\")) {\n    return d.text().as_string();\n  }\n  return \"\";\n}\n\nstd::string get_place_ref_or_geo(pugi::xml_node ref) {\n  if (auto const id = get_place_ref(ref); !id.empty()) {\n    return std::string{id};\n  }\n\n  auto const geo = ref.child(\"GeoPosition\");\n  utl::verify(geo,\n              \"PlaceRef.StopPlaceRef or PlaceRef.GeoPosition should be set\");\n\n  auto const lat_node = geo.child(\"siri:Latitude\");\n  auto const lon_node = geo.child(\"siri:Longitude\");\n  utl::verify(\n      lat_node && lon_node,\n      \"StopPlaceRef.GeoPosition needs siri:Latitude and siri:Longitude\");\n\n  auto const lat = lat_node.text().as_double();\n  auto const lng = lon_node.text().as_double();\n  return fmt::format(\"{},{}\", lat, lng);\n}\n\npugi::xml_node append_mode(pugi::xml_node service, transport_mode const m) {\n  auto const [transport_mode, submode_type, submode] = m;\n  auto mode = service.append_child(\"Mode\");\n  append(mode, \"PtMode\", transport_mode);\n  if (!submode_type.empty() && !submode.empty()) {\n    append(mode, fmt::format(\"siri:{}\", submode_type), submode);\n  }\n  return mode;\n}\n\nstd::string to_upper_ascii(std::string_view input) {\n  auto out = std::string{input};\n  std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {\n    return static_cast<char>(std::toupper(c));\n  });\n  return out;\n}\n\nstd::string now_timestamp() {\n  auto const now = std::chrono::system_clock::now();\n  return date::format(\"%FT%TZ\", date::floor<std::chrono::milliseconds>(now));\n}\n\nstd::string format_coord_param(double const lat, double const lon) {\n  auto out = std::ostringstream{};\n  out.setf(std::ios::fixed);\n  out << std::setprecision(6) << lat << \",\" << lon;\n  return out.str();\n}\n\nstd::string time_to_iso(openapi::date_time_t const& t) {\n  auto out = std::ostringstream{};\n  out << t;\n  return out.str();\n}\n\nstd::string duration_to_iso(std::chrono::seconds const dur) {\n  auto s = dur.count();\n  auto const h = s / 3600;\n  s -= h * 3600;\n  auto const m = s / 60;\n  s -= m * 60;\n  return fmt::format(\"PT{}{}{}\",  //\n                     h ? fmt::format(\"{}H\", h) : \"\",\n                     m ? fmt::format(\"{}M\", m) : \"\",\n                     s ? fmt::format(\"{}S\", s) : \"\");\n}\n\nstd::string xml_to_str(pugi::xml_document const& doc) {\n  auto out = std::ostringstream{};\n  doc.save(out, \"  \", pugi::format_indent);\n  auto result = out.str();\n  if (!result.empty() && result.back() == '\\n') {\n    result.pop_back();\n  }\n  return result;\n}\n\nstd::pair<pugi::xml_document, pugi::xml_node> create_ojp_response() {\n  auto doc = pugi::xml_document{};\n\n  auto decl = doc.append_child(pugi::node_declaration);\n  decl.append_attribute(\"version\").set_value(\"1.0\");\n  decl.append_attribute(\"encoding\").set_value(\"utf-8\");\n\n  auto ojp = doc.append_child(\"OJP\");\n  ojp.append_attribute(\"xmlns:siri\").set_value(\"http://www.siri.org.uk/siri\");\n  ojp.append_attribute(\"xmlns\").set_value(\"http://www.vdv.de/ojp\");\n  ojp.append_attribute(\"version\").set_value(\"2.0\");\n\n  auto response = ojp.append_child(\"OJPResponse\");\n  auto service_delivery = response.append_child(\"siri:ServiceDelivery\");\n  append(service_delivery, \"siri:ResponseTimestamp\", now_timestamp());\n  append(service_delivery, \"siri:ProducerRef\", \"MOTIS\");\n  append(service_delivery, \"siri:ResponseMessageIdentifier\", ++response_id);\n\n  return {std::move(doc), service_delivery};\n}\n\npugi::xml_document build_geocode_response(\n    std::string_view language, api::geocode_response const& matches) {\n  auto [doc, service_delivery] = create_ojp_response();\n\n  auto location_information =\n      service_delivery.append_child(\"OJPLocationInformationDelivery\");\n  append(location_information, \"siri:ResponseTimestamp\", now_timestamp());\n  append(location_information, \"siri:DefaultLanguage\", language);\n\n  for (auto const& match : matches) {\n    auto const stop_ref = match.id_;\n    auto const name = match.name_;\n    auto place_result = location_information.append_child(\"PlaceResult\");\n    auto place = place_result.append_child(\"Place\");\n    auto stop_place = place.append_child(\"StopPlace\");\n    stop_place.append_child(\"StopPlaceRef\").text().set(stop_ref);\n\n    append(language, stop_place, \"StopPlaceName\", name);\n\n    {\n      auto const private_code = stop_place.append_child(\"PrivateCode\");\n      append(private_code, \"System\", \"EFA\");\n      append(private_code, \"Value\", stop_ref);\n    }\n\n    append(place, \"TopographicPlaceRef\", \"n/a\");\n\n    append(language, place, \"Name\", name);\n    append_position(place, {match.lat_, match.lon_}, \"GeoPosition\");\n\n    if (match.modes_.has_value()) {\n      for (auto const& m : *match.modes_) {\n        append_mode(place, to_pt_mode(m));\n      }\n    }\n\n    place_result.append_child(\"Complete\").text().set(true);\n    place_result.append_child(\"Probability\").text().set(1);\n  }\n\n  return std::move(doc);\n}\n\npugi::xml_document build_map_stops_response(\n    std::string_view timestamp,\n    std::string_view language,\n    std::vector<api::Place> const& stops) {\n  auto [doc, service_delivery] = create_ojp_response();\n\n  auto loc_delivery =\n      service_delivery.append_child(\"OJPLocationInformationDelivery\");\n  loc_delivery.append_child(\"siri:ResponseTimestamp\")\n      .text()\n      .set(timestamp.data());\n  loc_delivery.append_child(\"siri:DefaultLanguage\").text().set(language);\n\n  for (auto const& stop : stops) {\n    auto place_result = loc_delivery.append_child(\"PlaceResult\");\n    auto place = place_result.append_child(\"Place\");\n\n    if (stop.stopId_.has_value()) {\n      auto stop_place = place.append_child(\"StopPlace\");\n      stop_place.append_child(\"StopPlaceRef\").text().set(*stop.stopId_);\n\n      auto stop_place_name = stop_place.append_child(\"StopPlaceName\");\n      auto stop_place_text = stop_place_name.append_child(\"Text\");\n      stop_place_text.append_attribute(\"xml:lang\").set_value(language);\n      stop_place_text.text().set(stop.name_);\n    }\n\n    auto place_text = place.append_child(\"Name\").append_child(\"Text\");\n    place_text.append_attribute(\"xml:lang\").set_value(language);\n    place_text.text().set(stop.name_);\n\n    append_position(place, {stop.lat_, stop.lon_}, \"GeoPosition\");\n\n    if (stop.modes_.has_value()) {\n      for (auto const& m : *stop.modes_) {\n        append_mode(place, to_pt_mode(m));\n      }\n    }\n\n    place_result.append_child(\"Complete\").text().set(true);\n    place_result.append_child(\"Probability\").text().set(1);\n  }\n\n  return std::move(doc);\n}\n\nvoid add_place(auto const& t,\n               hash_set<std::string>& already_added,\n               std::string_view language,\n               n::lang_t const& lang,\n               pugi::xml_node places_node,\n               api::Place const& p) {\n  auto const unique_id =\n      p.stopId_.value_or(fmt::format(\"{},{}\", p.lat_, p.lon_));\n  if (!already_added.insert(unique_id).second) {\n    return;\n  }\n\n  if (p.parentId_.has_value() && p.parentId_ != p.stopId_) {\n    add_place(t, already_added, language, lang, places_node,\n              to_place(maybe_ref(t.tt_), maybe_ref(t.tags_), t.w_, t.pl_,\n                       t.matches_, t.ae_, t.tz_, lang,\n                       tt_location{maybe_deref(t.tags_).get_location(\n                           maybe_deref(t.tt_), *p.parentId_)}));\n  }\n\n  auto place = places_node.append_child(\"Place\");\n  if (p.parentId_.has_value()) {\n    auto sp = place.append_child(\"StopPoint\");\n    append(sp, \"siri:StopPointRef\", p.stopId_.value());\n    append(language, sp, \"StopPointName\", p.name_);\n    if (p.parentId_.has_value()) {\n      append(sp, \"ParentRef\", *p.parentId_);\n    }\n  } else {\n    auto sp = place.append_child(\"StopPlace\");\n    append(sp, \"siri:StopPlaceRef\", p.stopId_.value());\n    append(language, sp, \"StopPlaceName\", p.name_);\n    if (p.parentId_.has_value()) {\n      append(sp, \"ParentRef\", *p.parentId_);\n    }\n  }\n  append(language, place, \"Name\", p.name_);\n  append_position(place, {p.lat_, p.lon_}, \"GeoPosition\");\n}\n\nvoid append_leg_places(auto const& t,\n                       hash_set<std::string>& already_added,\n                       std::string_view language,\n                       n::lang_t const& lang,\n                       pugi::xml_node places_node,\n                       api::Leg const& leg,\n                       bool const include_calls) {\n  for (auto const& stop : {leg.from_, leg.to_}) {\n    if (stop.stopId_.has_value()) {\n      add_place(t, already_added, language, lang, places_node, stop);\n    }\n  }\n\n  if (include_calls && leg.intermediateStops_.has_value()) {\n    for (auto const& stop : *leg.intermediateStops_) {\n      add_place(t, already_added, language, lang, places_node, stop);\n    }\n  }\n}\n\npugi::xml_document build_trip_info_response(trip const& trip_ep,\n                                            std::string_view language,\n                                            std::string_view operating_day,\n                                            std::string_view journey_ref,\n                                            api::Itinerary const& itinerary,\n                                            bool const include_calls,\n                                            bool const include_service,\n                                            bool const include_track,\n                                            bool const include_places,\n                                            bool const include_situations) {\n  auto [doc, service_delivery] = create_ojp_response();\n\n  auto delivery = service_delivery.append_child(\"OJPTripInfoDelivery\");\n  delivery.append_child(\"siri:ResponseTimestamp\").text().set(now_timestamp());\n  delivery.append_child(\"siri:DefaultLanguage\").text().set(language.data());\n\n  auto const& leg = itinerary.legs_.at(0);\n  auto const lang = n::lang_t{{std::string{language}}};\n\n  if (include_places || include_situations) {\n    auto ctx = delivery.append_child(\"TripInfoResponseContext\");\n\n    if (include_places) {\n      auto already_added = hash_set<std::string>{};\n      auto places_node = ctx.append_child(\"Places\");\n      append_leg_places(trip_ep, already_added, language, lang, places_node,\n                        leg, include_calls);\n    }\n\n    if (include_situations) {\n      ctx.append_child(\"Situations\");\n    }\n  }\n\n  auto result = delivery.append_child(\"TripInfoResult\");\n\n  if (include_calls) {\n    auto add_call = [&, n = 0](api::Place const& place) mutable {\n      auto c = result.append_child(\"PreviousCall\");\n      append_stop_ref(c, place);\n      append(language, c, \"StopPointName\", place.name_);\n      append(language, c, \"PlannedQuay\", place.scheduledTrack_);\n      append(language, c, \"NameSuffix\",\n             \"PLATFORM_ACCESS_WITHOUT_ASSISTANCE\");  // TODO real data\n\n      auto arr = c.append_child(\"ServiceArrival\");\n      append(arr, \"TimetabledTime\",\n             place.scheduledArrival_.transform(time_to_iso));\n      append(arr, \"EstimatedTime\", place.arrival_.transform(time_to_iso));\n\n      auto dep = c.append_child(\"ServiceDeparture\");\n      append(dep, \"TimetabledTime\",\n             place.scheduledDeparture_.transform(time_to_iso));\n      append(dep, \"EstimatedTime\", place.departure_.transform(time_to_iso));\n\n      c.append_child(\"Order\").text().set(++n);\n    };\n\n    add_call(leg.from_);\n    for (auto const& stop : leg.intermediateStops_.value()) {\n      add_call(stop);\n    }\n    add_call(leg.to_);\n  }\n\n  if (include_service) {\n    auto service = result.append_child(\"Service\");\n    append(service, \"OperatingDayRef\", operating_day);\n    append(service, \"JourneyRef\", journey_ref);\n    append(service, \"PublicCode\", leg.routeShortName_);\n    append(service, \"siri:LineRef\", leg.routeId_);\n    append(service, \"siri:DirectionRef\", leg.directionId_);\n\n    auto mode =\n        append_mode(service, get_transport_mode(leg.routeType_.value()));\n    append(language, mode, \"Name\",\n           leg.category_.transform([](auto&& x) { return x.name_; }));\n    append(language, mode, \"ShortName\",\n           leg.category_.transform([](auto&& x) { return x.shortName_; }));\n\n    append(language, service, \"PublishedServiceName\", leg.displayName_.value());\n    append(service, \"TrainNumber\", leg.tripShortName_);\n    append(language, service, \"OriginText\", leg.tripFrom_.value().name_);\n    append(service, \"siri:OperatorRef\", leg.agencyId_);\n    append(\n        service, \"DestinationStopPointRef\",\n        leg.tripTo_.transform([](api::Place const& to) { return to.stopId_; })\n            .value());\n    append(language, service, \"DestinationText\", leg.headsign_);\n  }\n\n  if (include_track) {\n    auto section =\n        result.append_child(\"JourneyTrack\").append_child(\"TrackSection\");\n\n    auto start = section.append_child(\"TrackSectionStart\");\n    append_stop_ref(start, leg.from_);\n    append(language, start, \"Name\", leg.from_.name_);\n\n    auto end = section.append_child(\"TrackSectionEnd\");\n    append_stop_ref(end, leg.to_);\n    append(language, end, \"Name\", leg.to_.name_);\n\n    auto link = section.append_child(\"LinkProjection\");\n    for (auto const& pos : geo::decode_polyline<6>(leg.legGeometry_.points_)) {\n      append_position(link, pos);\n    }\n\n    append(section, \"Duration\",\n           duration_to_iso(std::chrono::seconds{leg.duration_}));\n    append(section, \"Length\", leg.distance_.value_or(0.0));\n  }\n\n  return std::move(doc);\n}\n\npugi::xml_document build_stop_event_response(\n    stop_times const& stop_times_ep,\n    std::string_view language,\n    bool const include_previous_calls,\n    bool const include_onward_calls,\n    bool const include_situations,\n    api::stoptimes_response const& stop_times_res) {\n  auto [doc, service_delivery] = create_ojp_response();\n\n  auto delivery = service_delivery.append_child(\"OJPStopEventDelivery\");\n  delivery.append_child(\"siri:ResponseTimestamp\").text().set(now_timestamp());\n  delivery.append_child(\"siri:DefaultLanguage\").text().set(language.data());\n\n  auto const lang = n::lang_t{{std::string{language}}};\n\n  {\n    auto ctx = delivery.append_child(\"StopEventResponseContext\");\n    auto places_node = ctx.append_child(\"Places\");\n    auto added = hash_set<std::string>{};\n\n    for (auto const& st : stop_times_res.stopTimes_) {\n      add_place(stop_times_ep, added, language, lang, places_node, st.place_);\n      if (include_previous_calls && st.previousStops_.has_value()) {\n        for (auto const& p : *st.previousStops_) {\n          add_place(stop_times_ep, added, language, lang, places_node, p);\n        }\n      }\n      if (include_onward_calls && st.nextStops_.has_value()) {\n        for (auto const& p : *st.nextStops_) {\n          add_place(stop_times_ep, added, language, lang, places_node, p);\n        }\n      }\n    }\n\n    if (include_situations) {\n      ctx.append_child(\"Situations\");\n    }\n  }\n\n  auto idx = 0;\n  for (auto const& st : stop_times_res.stopTimes_) {\n    auto result = delivery.append_child(\"StopEventResult\");\n    append(result, \"Id\", ++idx);\n\n    auto add_call = [&, order = 0](pugi::xml_node parent,\n                                   api::Place const& place) mutable {\n      auto call = parent.append_child(\"CallAtStop\");\n      append_stop_ref(call, place);\n      append(call, \"StopPointName\", place.name_);\n      if (place.scheduledTrack_.has_value()) {\n        append(call, \"PlannedQuay\", place.scheduledTrack_);\n      }\n\n      if (place.scheduledArrival_ || place.arrival_) {\n        auto arr = call.append_child(\"ServiceArrival\");\n        append(arr, \"TimetabledTime\",\n               place.scheduledArrival_.transform(time_to_iso));\n        append(arr, \"EstimatedTime\", place.arrival_.transform(time_to_iso));\n      }\n\n      if (place.scheduledDeparture_ || place.departure_) {\n        auto dep = call.append_child(\"ServiceDeparture\");\n        append(dep, \"TimetabledTime\",\n               place.scheduledDeparture_.transform(time_to_iso));\n        append(dep, \"EstimatedTime\", place.departure_.transform(time_to_iso));\n      }\n\n      append(call, \"Order\", ++order);\n    };\n\n    auto stop_event = result.append_child(\"StopEvent\");\n\n    if (include_previous_calls && st.previousStops_.has_value()) {\n      for (auto const& p : *st.previousStops_) {\n        add_call(stop_event.append_child(\"PreviousCall\"), p);\n      }\n    }\n\n    add_call(stop_event.append_child(\"ThisCall\"), st.place_);\n\n    if (include_onward_calls && st.nextStops_.has_value()) {\n      for (auto const& p : *st.nextStops_) {\n        add_call(stop_event.append_child(\"OnwardCall\"), p);\n      }\n    }\n\n    auto service = stop_event.append_child(\"Service\");\n    auto const trip_id = split_trip_id(st.tripId_);\n    append(service, \"OperatingDayRef\", trip_id.start_date_);\n    append(service, \"JourneyRef\", st.tripId_);\n\n    auto const public_code =\n        !st.routeShortName_.empty()\n            ? st.routeShortName_\n            : (!st.displayName_.empty() ? st.displayName_ : st.routeLongName_);\n    if (!public_code.empty()) {\n      append(service, \"PublicCode\", public_code);\n    }\n\n    append(service, \"PublicCode\", st.routeShortName_);\n    append(service, \"siri:LineRef\", st.routeId_);\n    append(service, \"siri:DirectionRef\", st.directionId_);\n\n    auto mode = append_mode(\n        service,\n        st.routeType_.has_value()\n            ? get_transport_mode(static_cast<std::uint16_t>(*st.routeType_))\n            : to_pt_mode(st.mode_));\n    append(language, mode, \"Name\", st.displayName_);\n    append(language, mode, \"ShortName\", st.routeShortName_);\n\n    append(language, service, \"PublishedServiceName\", st.displayName_);\n    append(service, \"TrainNumber\", st.tripShortName_);\n    append(language, service, \"OriginText\",\n           st.previousStops_.and_then([](std::vector<api::Place> const& x) {\n             return x.empty() ? std::nullopt : std::optional{x.front().name_};\n           }));\n    append(service, \"siri:OperatorRef\", st.agencyId_);\n    append(language, service, \"DestinationText\", st.headsign_);\n  }\n\n  return std::move(doc);\n}\n\npugi::xml_document build_trip_response(routing const& routing_ep,\n                                       std::string_view language,\n                                       api::plan_response const& plan_res,\n                                       bool const include_track_sections,\n                                       bool const include_leg_projection,\n                                       bool const include_intermediate_stops) {\n  auto [doc, service_delivery] = create_ojp_response();\n\n  auto delivery = service_delivery.append_child(\"OJPTripDelivery\");\n  append(delivery, \"siri:ResponseTimestamp\", now_timestamp());\n  append(delivery, \"siri:DefaultLanguage\", language.data());\n\n  auto const lang = n::lang_t{{std::string{language}}};\n  auto ctx = delivery.append_child(\"TripResponseContext\");\n  auto places_node = ctx.append_child(\"Places\");\n  auto added = hash_set<std::string>{};\n  for (auto const& it : plan_res.itineraries_) {\n    for (auto const& leg : it.legs_) {\n      append_leg_places(routing_ep, added, language, lang, places_node, leg,\n                        include_intermediate_stops);\n    }\n  }\n\n  auto trip_idx = 0;\n  for (auto const& it : plan_res.itineraries_) {\n    auto const id = ++trip_idx;\n    auto result = delivery.append_child(\"TripResult\");\n    result.append_child(\"Id\").text().set(id);\n\n    auto trip = result.append_child(\"Trip\");\n    append(trip, \"Id\", id);\n    append(trip, \"Duration\",\n           duration_to_iso(std::chrono::seconds{it.duration_}));\n    append(trip, \"StartTime\", time_to_iso(it.startTime_));\n    append(trip, \"EndTime\", time_to_iso(it.endTime_));\n    append(trip, \"Transfers\", it.transfers_);\n    append(\n        trip, \"Distance\",\n        sr::fold_left(it.legs_, 0.0, [](double const sum, api::Leg const& l) {\n          return sum + l.legGeometry_.length_;\n        }));\n\n    auto leg_idx = 0;\n    auto leg_pos = 0U;\n    auto const leg_count = it.legs_.size();\n    api::Leg const* prev = nullptr;\n    for (auto const& leg : it.legs_) {\n      if (leg.displayName_.has_value()) {\n        if (leg.interlineWithPreviousLeg_.value_or(false) && prev != nullptr) {\n          auto leg_node = trip.append_child(\"Leg\");\n          append(leg_node, \"Id\", ++leg_idx);\n          append(leg_node, \"Duration\",\n                 duration_to_iso(std::chrono::seconds{leg.duration_}));\n\n          auto transfer_leg = leg_node.append_child(\"TransferLeg\");\n\n          append(transfer_leg, \"TransferType\", \"remainInVehicle\");\n\n          auto leg_start = transfer_leg.append_child(\"LegStart\");\n          append_stop_ref(leg_start, prev->to_);\n          append(language, leg_start, \"Name\", prev->to_.name_);\n\n          auto leg_end = transfer_leg.append_child(\"LegEnd\");\n          append_stop_ref(leg_end, leg.from_);\n          append(language, leg_end, \"Name\", leg.from_.name_);\n\n          append(transfer_leg, \"Duration\",\n                 duration_to_iso(leg.startTime_.time_ - prev->endTime_.time_));\n\n          auto const co2 = leg_node.append_child(\"EmissionCO2\");\n          append(co2, \"KilogramPerPersonKm\", 0);\n        }\n\n        auto leg_node = trip.append_child(\"Leg\");\n        append(leg_node, \"Id\", ++leg_idx);\n        append(leg_node, \"Duration\",\n               duration_to_iso(std::chrono::seconds{leg.duration_}));\n\n        auto add_call = [&, order = 0](pugi::xml_node parent,\n                                       api::Place const& place) mutable {\n          append_stop_ref(parent, place);\n          append(language, parent, \"StopPointName\", place.name_);\n          if (place.scheduledTrack_.has_value()) {\n            append(language, parent, \"PlannedQuay\", *place.scheduledTrack_);\n          }\n\n          if (place.departure_) {\n            auto dep = parent.append_child(\"ServiceDeparture\");\n            append(dep, \"TimetabledTime\",\n                   place.scheduledDeparture_.transform(time_to_iso));\n            append(dep, \"EstimatedTime\",\n                   place.departure_.transform(time_to_iso));\n          }\n\n          if (place.arrival_) {\n            auto arr = parent.append_child(\"ServiceArrival\");\n            append(arr, \"TimetabledTime\",\n                   place.scheduledArrival_.transform(time_to_iso));\n            append(arr, \"EstimatedTime\", place.arrival_.transform(time_to_iso));\n          }\n\n          parent.append_child(\"Order\").text().set(++order);\n        };\n\n        auto timed_leg = leg_node.append_child(\"TimedLeg\");\n        add_call(timed_leg.append_child(\"LegBoard\"), leg.from_);\n        for (auto const& stop : *leg.intermediateStops_) {\n          add_call(timed_leg.append_child(\"LegIntermediate\"), stop);\n        }\n        add_call(timed_leg.append_child(\"LegAlight\"), leg.to_);\n\n        auto service = timed_leg.append_child(\"Service\");\n        auto const [start_day, _, _1, _2] = split_trip_id(leg.tripId_.value());\n        append(service, \"OperatingDayRef\", start_day);\n        append(service, \"JourneyRef\", leg.tripId_);\n        append(service, \"LineRef\", leg.routeId_);\n        append(service, \"DirectionRef\", leg.directionId_);\n        append(service, \"siri:OperatorRef\", leg.agencyId_);\n\n        {\n          auto const category = service.append_child(\"ProductCategory\");\n          append(language, category, \"Name\",\n                 leg.category_.transform([](auto&& x) { return x.name_; }));\n          append(\n              language, category, \"ShortName\",\n              leg.category_.transform([](auto&& x) { return x.shortName_; }));\n          append(language, category, \"ProductCategoryRef\",\n                 leg.category_.transform([](auto&& x) { return x.id_; }));\n        }\n\n        append(language, service, \"DestinationText\", leg.headsign_);\n\n        // TODO\n        // <Attribute>\n        //         <UserText>\n        //                 <Text xml:lang=\"de\">Niederflureinstieg</Text>\n        //         </UserText>\n        //         <Code>A__NF</Code>\n        //         <Importance>50</Importance>\n        // </Attribute>\n\n        append(language, service, \"PublishedServiceName\", leg.displayName_);\n\n        {\n          auto mode =\n              append_mode(service, get_transport_mode(leg.routeType_.value()));\n          append(language, mode, \"Name\",\n                 leg.category_.transform([](auto&& x) { return x.name_; }));\n          append(\n              language, mode, \"ShortName\",\n              leg.category_.transform([](auto&& x) { return x.shortName_; }));\n        }\n\n        if (include_track_sections || include_leg_projection) {\n          auto leg_track = timed_leg.append_child(\"LegTrack\");\n          auto section = leg_track.append_child(\"TrackSection\");\n\n          auto start = section.append_child(\"TrackSectionStart\");\n          append_stop_ref(start, leg.from_);\n          append(language, start, \"Name\", leg.from_.name_);\n\n          auto end = section.append_child(\"TrackSectionEnd\");\n          append_stop_ref(start, leg.to_);\n          append(language, end, \"Name\", leg.to_.name_);\n\n          if (include_leg_projection) {\n            auto link = section.append_child(\"LinkProjection\");\n            for (auto const& pos :\n                 geo::decode_polyline<6>(leg.legGeometry_.points_)) {\n              append_position(link, pos);\n            }\n          }\n\n          append(section, \"Duration\",\n                 duration_to_iso(std::chrono::seconds{leg.duration_}));\n          append(section, \"Length\", leg.legGeometry_.length_);\n        }\n      } else {\n        auto const is_first_or_last =\n            (leg_pos == 0U) || (leg_pos + 1U == leg_count);\n        auto leg_node = trip.append_child(\"Leg\");\n        append(leg_node, \"Id\", ++leg_idx);\n        append(leg_node, \"Duration\",\n               duration_to_iso(std::chrono::seconds{leg.duration_}));\n\n        if (is_first_or_last) {\n          auto continuous_leg = leg_node.append_child(\"ContinuousLeg\");\n          auto leg_start = continuous_leg.append_child(\"LegStart\");\n          append_place_ref_or_geo(leg_start, leg.from_);\n          append(language, leg_start, \"Name\", leg.from_.name_);\n\n          auto leg_end = continuous_leg.append_child(\"LegEnd\");\n          append_place_ref_or_geo(leg_end, leg.to_);\n          append(language, leg_end, \"Name\", leg.to_.name_);\n\n          auto service = continuous_leg.append_child(\"Service\");\n          append(service, \"PersonalModeOfOperation\", \"own\");\n          append(service, \"PersonalMode\", \"foot\");\n\n          append(continuous_leg, \"Duration\",\n                 duration_to_iso(std::chrono::seconds{leg.duration_}));\n          append(continuous_leg, \"Length\", leg.legGeometry_.length_);\n\n          auto leg_track = continuous_leg.append_child(\"LegTrack\");\n          auto track_section = leg_track.append_child(\"TrackSection\");\n\n          auto track_section_start =\n              track_section.append_child(\"TrackSectionStart\");\n          append_place_ref_or_geo(track_section_start, leg.from_);\n          append(language, track_section_start, \"Name\", leg.from_.name_);\n\n          auto track_section_end =\n              track_section.append_child(\"TrackSectionEnd\");\n          append_place_ref_or_geo(track_section_end, leg.to_);\n          append(language, track_section_end, \"Name\", leg.to_.name_);\n\n          auto link_projection = track_section.append_child(\"LinkProjection\");\n          for (auto const& pos :\n               geo::decode_polyline<6>(leg.legGeometry_.points_)) {\n            append_position(link_projection, pos);\n          }\n\n          append(track_section, \"Duration\",\n                 duration_to_iso(std::chrono::seconds{leg.duration_}));\n          append(track_section, \"Length\", leg.legGeometry_.length_);\n        } else {\n          auto transfer_leg = leg_node.append_child(\"TransferLeg\");\n          append(transfer_leg, \"TransferType\", \"walk\");\n\n          auto leg_start = transfer_leg.append_child(\"LegStart\");\n          append_place_ref_or_geo(leg_start, leg.from_);\n          append(language, leg_start, \"Name\", leg.from_.name_);\n\n          auto leg_end = transfer_leg.append_child(\"LegEnd\");\n          append_place_ref_or_geo(leg_end, leg.to_);\n          append(language, leg_end, \"Name\", leg.to_.name_);\n\n          append(transfer_leg, \"Duration\",\n                 duration_to_iso(std::chrono::seconds{leg.duration_}));\n\n          auto path_guidance = transfer_leg.append_child(\"PathGuidance\");\n          auto track_section = path_guidance.append_child(\"TrackSection\");\n\n          auto track_section_start =\n              track_section.append_child(\"TrackSectionStart\");\n          append_place_ref_or_geo(track_section_start, leg.from_);\n          append(language, track_section_start, \"Name\", leg.from_.name_);\n\n          auto track_section_end =\n              track_section.append_child(\"TrackSectionEnd\");\n          append_place_ref_or_geo(track_section_end, leg.to_);\n          append(language, track_section_end, \"Name\", leg.to_.name_);\n\n          auto link_projection = track_section.append_child(\"LinkProjection\");\n          for (auto const& pos :\n               geo::decode_polyline<6>(leg.legGeometry_.points_)) {\n            append_position(link_projection, pos);\n          }\n\n          append(track_section, \"Duration\",\n                 duration_to_iso(std::chrono::seconds{leg.duration_}));\n          append(track_section, \"Length\", leg.legGeometry_.length_);\n        }\n      }\n      prev = &leg;\n      ++leg_pos;\n    }\n  }\n\n  return std::move(doc);\n}\n\nnet::reply ojp::operator()(net::route_request const& http_req, bool) const {\n  auto xml = pugi::xml_document{};\n  xml.load_string(http_req.body().c_str());\n\n  auto const req =\n      xml.child(\"OJP\").child(\"OJPRequest\").child(\"siri:ServiceRequest\");\n  utl::verify<net::bad_request_exception>(\n      req, \"no OJPReuqest > siri:ServiceRequest found\");\n\n  auto const context = req.child(\"siri:ServiceRequestContext\");\n  auto const language = context.child(\"siri:Language\").text().as_string();\n  auto const lang = language ? language : std::string{\"en\"};\n\n  auto response = pugi::xml_document{};\n  if (auto const loc_req = req.child(\"OJPLocationInformationRequest\");\n      loc_req) {\n    auto const input = loc_req.child(\"InitialInput\");\n    if (auto const geo = input.child(\"GeoRestriction\"); geo) {\n      utl::verify(stops_ep_.has_value(), \"stops not loaded\");\n\n      auto const rect = geo.child(\"Rectangle\");\n      auto const upper_left = rect.child(\"UpperLeft\");\n      auto const lower_right = rect.child(\"LowerRight\");\n      utl::verify<net::bad_request_exception>(upper_left && lower_right,\n                                              \"missing GeoRestriction box\");\n\n      auto url = boost::urls::url{\"/api/v1/map/stop\"};\n      auto params = url.params();\n      params.append(\n          {\"min\",\n           fmt::format(\"{},{}\",\n                       lower_right.child(\"siri:Latitude\").text().as_double(),\n                       upper_left.child(\"siri:Longitude\").text().as_double())});\n      params.append(\n          {\"max\",\n           fmt::format(\n               \"{},{}\", upper_left.child(\"siri:Latitude\").text().as_double(),\n               lower_right.child(\"siri:Longitude\").text().as_double())});\n      params.append({\"language\", lang});\n\n      response =\n          build_map_stops_response(now_timestamp(), lang, (*stops_ep_)(url));\n    } else if (auto const stop_id = loc_req.child(\"PlaceRef\")\n                                        .child(\"StopPlaceRef\")\n                                        .text()\n                                        .as_string();\n               stop_id && strlen(stop_id) != 0U) {\n      auto const& tt = *geocoding_ep_->tt_;\n      auto const& tags = geocoding_ep_->tags_;\n      auto const stop = tags->get_location(tt, stop_id);\n      auto const pos = tt.locations_.coordinates_.at(stop);\n      response = build_geocode_response(\n          language, std::vector<api::Match>{api::Match{\n                        .type_ = api::LocationTypeEnum::STOP,\n                        .name_ = std::string{tt.get_default_translation(\n                            tt.locations_.names_.at(stop))},\n                        .id_ = tags->id(tt, stop),\n                        .lat_ = pos.lat(),\n                        .lon_ = pos.lng(),\n                    }});\n    } else {\n      auto const name = input.child(\"Name\").text();\n\n      utl::verify(geocoding_ep_.has_value(), \"geocoding not loaded\");\n\n      auto url = boost::urls::url{\"/api/v1/geocode\"};\n      auto params = url.params();\n      params.append({\"text\", name.as_string()});\n      params.append({\"language\", lang});\n      params.append({\"type\", \"STOP\"});\n\n      response = build_geocode_response(lang, (*geocoding_ep_)(url));\n    }\n  } else if (auto const trip_info_req = req.child(\"OJPTripInfoRequest\")) {\n    utl::verify(trip_ep_.has_value(), \"trip not loaded\");\n\n    auto const journey_ref =\n        trip_info_req.child(\"JourneyRef\").text().as_string();\n    auto const operating_day =\n        trip_info_req.child(\"OperatingDayRef\").text().as_string();\n\n    auto const params = trip_info_req.child(\"Params\");\n\n    auto url = boost::urls::url{\"/api/v5/trip\"};\n    auto url_params = url.params();\n    url_params.append({\"tripId\", journey_ref});\n    url_params.append({\"language\", lang});\n\n    response = build_trip_info_response(\n        *trip_ep_, lang, operating_day, journey_ref, (*trip_ep_)(url),\n        params.child(\"IncludeCalls\").text().as_bool(true),\n        params.child(\"IncludeService\").text().as_bool(true),\n        params.child(\"IncludeTrackProjection\").text().as_bool(true),\n        params.child(\"IncludePlacesContext\").text().as_bool(true),\n        params.child(\"IncludeSituationsContext\").text().as_bool(true));\n  } else if (auto const plan_req = req.child(\"OJPTripRequest\")) {\n    utl::verify(routing_ep_.has_value(), \"routing not loaded\");\n\n    auto const origin = plan_req.child(\"Origin\");\n    auto const destination = plan_req.child(\"Destination\");\n    auto const origin_ref = origin.child(\"PlaceRef\");\n    auto const destination_ref = destination.child(\"PlaceRef\");\n\n    auto const dep_time =\n        std::string_view{origin.child(\"DepArrTime\").text().as_string()};\n    auto const arr_time =\n        std::string_view{destination.child(\"DepArrTime\").text().as_string()};\n\n    auto const params = plan_req.child(\"Params\");\n    auto const num_results = params.child(\"NumberOfResults\").text().as_int(5);\n    auto const include_track_sections =\n        params.child(\"IncludeTrackSections\").text().as_bool(false);\n    auto const include_leg_projection =\n        params.child(\"IncludeLegProjection\").text().as_bool(false);\n    auto const include_intermediate_stops =\n        params.child(\"IncludeIntermediateStops\").text().as_bool(false);\n\n    auto url = boost::urls::url{\"/api/v5/plan\"};\n    auto url_params = url.params();\n    url_params.append({\"fromPlace\", get_place_ref_or_geo(origin_ref)});\n    url_params.append({\"toPlace\", get_place_ref_or_geo(destination_ref)});\n    url_params.append({\"time\", !dep_time.empty() ? dep_time : arr_time});\n    url_params.append({\"numItineraries\", fmt::format(\"{}\", num_results)});\n    url_params.append({\"joinInterlinedLegs\", \"false\"});\n    url_params.append({\"language\", lang});\n    if (dep_time.empty()) {\n      url_params.append({\"arriveBy\", \"true\"});\n    }\n\n    response = build_trip_response(\n        *routing_ep_, lang, (*routing_ep_)(url), include_track_sections,\n        include_leg_projection, include_intermediate_stops);\n  } else if (auto const stop_times_req = req.child(\"OJPStopEventRequest\")) {\n    utl::verify(stop_times_ep_.has_value(), \"stop times not loaded\");\n\n    auto const location = stop_times_req.child(\"Location\");\n    auto const place_ref = location.child(\"PlaceRef\");\n    auto const stop_id = get_place_ref(place_ref);\n    auto const dep_arr_time = location.child(\"DepArrTime\").text().as_string();\n\n    auto const params = stop_times_req.child(\"Params\");\n    auto const number_of_results = params.child(\"NumberOfResults\");\n    auto const stop_event_type =\n        params.child(\"StopEventType\").text().as_string();\n    auto const include_prev =\n        params.child(\"IncludePreviousCalls\").text().as_bool(false);\n    auto const include_onward =\n        params.child(\"IncludeOnwardCalls\").text().as_bool(false);\n\n    auto url = boost::urls::url{\"/api/v5/stoptimes\"};\n    auto url_params = url.params();\n    url_params.append({\"stopId\", stop_id});\n    url_params.append({\"language\", lang});\n    if (number_of_results) {\n      url_params.append(\n          {\"n\", fmt::to_string(number_of_results.text().as_int())});\n    }\n    if (dep_arr_time) {\n      url_params.append({\"time\", dep_arr_time});\n    }\n    url_params.append({\"fetchStops\", \"true\"});\n    if (stop_event_type == \"arrival\"sv) {\n      url_params.append({\"arriveBy\", \"true\"});\n    }\n\n    response =\n        build_stop_event_response(*stop_times_ep_, lang, include_prev,\n                                  include_onward, true, (*stop_times_ep_)(url));\n  } else {\n    throw net::bad_request_exception{\"unsupported OJP request\"};\n  }\n\n  auto reply = net::web_server::string_res_t{boost::beast::http::status::ok,\n                                             http_req.version()};\n  reply.insert(boost::beast::http::field::content_type,\n               \"text/xml; charset=utf-8\");\n  net::set_response_body(reply, http_req, xml_to_str(response));\n  reply.keep_alive(http_req.keep_alive());\n  return reply;\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/one_to_all.cc",
    "content": "#include \"motis/endpoints/one_to_all.h\"\n\n#include <chrono>\n#include <vector>\n\n#include \"utl/verify.h\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/too_many_exception.h\"\n\n#include \"nigiri/common/delta_t.h\"\n#include \"nigiri/routing/limits.h\"\n#include \"nigiri/routing/one_to_all.h\"\n#include \"nigiri/routing/query.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/gbfs/routing_data.h\"\n#include \"motis/metrics_registry.h\"\n#include \"motis/place.h\"\n#include \"motis/timetable/modes_to_clasz_mask.h\"\n\nnamespace motis::ep {\n\nnamespace n = nigiri;\n\napi::Reachable one_to_all::operator()(boost::urls::url_view const& url) const {\n  metrics_->routing_requests_.Increment();\n\n  auto const max_travel_minutes =\n      config_.get_limits().onetoall_max_travel_minutes_;\n  auto const query = api::oneToAll_params{url.params()};\n  utl::verify<net::too_many_exception>(\n      query.maxTravelTime_ <= max_travel_minutes,\n      \"maxTravelTime too large ({} > {}). The server admin can change \"\n      \"this limit in config.yml with 'onetoall_max_travel_minutes'. \"\n      \"See documentation for details.\",\n      query.maxTravelTime_, max_travel_minutes);\n  if (query.maxTransfers_.has_value()) {\n    utl::verify<net::bad_request_exception>(query.maxTransfers_ >= 0U,\n                                            \"maxTransfers < 0: {}\",\n                                            *query.maxTransfers_);\n    utl::verify<net::too_many_exception>(\n        query.maxTransfers_ <= n::routing::kMaxTransfers,\n        \"maxTransfers > {}: {}\", n::routing::kMaxTransfers,\n        *query.maxTransfers_);\n  }\n\n  auto const unreachable = query.arriveBy_\n                               ? n::kInvalidDelta<n::direction::kBackward>\n                               : n::kInvalidDelta<n::direction::kForward>;\n\n  auto const make_place = [&](place_t const& p, n::unixtime_t const t,\n                              n::event_type const ev) {\n    auto place = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, {}, p);\n    if (ev == n::event_type::kArr) {\n      place.arrival_ = t;\n    } else {\n      place.departure_ = t;\n    }\n    return place;\n  };\n\n  auto const time = std::chrono::time_point_cast<std::chrono::minutes>(\n      *query.time_.value_or(openapi::now()));\n  auto const max_travel_time = n::duration_t{query.maxTravelTime_};\n  auto const one = get_place(&tt_, &tags_, query.one_);\n  auto const one_modes = deduplicate(query.arriveBy_ ? query.postTransitModes_\n                                                     : query.preTransitModes_);\n  auto const one_max_time = std::min(\n      std::chrono::seconds{query.arriveBy_ ? query.maxPostTransitTime_\n                                           : query.maxPreTransitTime_},\n      std::min(\n          std::chrono::duration_cast<std::chrono::seconds>(max_travel_time),\n          std::chrono::seconds{\n              config_.get_limits()\n                  .street_routing_max_prepost_transit_seconds_}));\n  auto const one_dir =\n      query.arriveBy_ ? osr::direction::kBackward : osr::direction::kForward;\n\n  auto const r = routing{\n      config_, w_,        l_,      pl_,      elevations_,  &tt_,    nullptr,\n      &tags_,  loc_tree_, fa_,     matches_, way_matches_, rt_,     nullptr,\n      gbfs_,   nullptr,   nullptr, nullptr,  nullptr,      metrics_};\n  auto gbfs_rd = gbfs::gbfs_routing_data{w_, l_, gbfs_};\n\n  auto const osr_params = get_osr_parameters(query);\n  auto prepare_stats = std::map<std::string, std::uint64_t>{};\n  auto q = n::routing::query{\n      .start_time_ = time,\n      .start_match_mode_ = get_match_mode(r, one),\n      .start_ = r.get_offsets(\n          nullptr, one, one_dir, one_modes, std::nullopt, std::nullopt,\n          std::nullopt, std::nullopt, false, osr_params,\n          query.pedestrianProfile_, query.elevationCosts_, one_max_time,\n          query.maxMatchingDistance_, gbfs_rd, prepare_stats),\n      .td_start_ = r.get_td_offsets(\n          nullptr, nullptr, one, one_dir, one_modes, osr_params,\n          query.pedestrianProfile_, query.elevationCosts_,\n          query.maxMatchingDistance_, one_max_time, time, prepare_stats),\n      .max_transfers_ = static_cast<std::uint8_t>(\n          query.maxTransfers_.value_or(n::routing::kMaxTransfers)),\n      .max_travel_time_ = max_travel_time,\n      .prf_idx_ = static_cast<n::profile_idx_t>(\n          query.useRoutedTransfers_\n              ? (query.pedestrianProfile_ ==\n                         api::PedestrianProfileEnum::WHEELCHAIR\n                     ? 2U\n                     : 1U)\n              : 0U),\n      .allowed_claszes_ = to_clasz_mask(query.transitModes_),\n      .require_bike_transport_ = query.requireBikeTransport_,\n      .require_car_transport_ = query.requireCarTransport_,\n      .transfer_time_settings_ =\n          n::routing::transfer_time_settings{\n              .default_ = (query.minTransferTime_ == 0 &&\n                           query.additionalTransferTime_ == 0 &&\n                           query.transferTimeFactor_ == 1.0),\n              .min_transfer_time_ = n::duration_t{query.minTransferTime_},\n              .additional_time_ = n::duration_t{query.additionalTransferTime_},\n              .factor_ = static_cast<float>(query.transferTimeFactor_)},\n  };\n\n  if (tt_.locations_.footpaths_out_.at(q.prf_idx_).empty()) {\n    q.prf_idx_ = 0U;\n  }\n\n  auto const state =\n      query.arriveBy_\n          ? n::routing::one_to_all<n::direction::kBackward>(tt_, nullptr, q)\n          : n::routing::one_to_all<n::direction::kForward>(tt_, nullptr, q);\n\n  auto reachable = nigiri::bitvec{tt_.n_locations()};\n  for (auto i = 0U; i != tt_.n_locations(); ++i) {\n    if (state.get_best<0>()[i][0] != unreachable) {\n      reachable.set(i);\n    }\n  }\n\n  auto const max_results = config_.get_limits().onetoall_max_results_;\n  utl::verify<net::too_many_exception>(reachable.count() <= max_results,\n                                       \"too many results: {} > {}\",\n                                       reachable.count(), max_results);\n\n  auto all = std::vector<api::ReachablePlace>{};\n  all.reserve(reachable.count());\n  auto const all_ev =\n      query.arriveBy_ ? n::event_type::kDep : n::event_type::kArr;\n  reachable.for_each_set_bit([&](auto const i) {\n    auto const l = n::location_idx_t{i};\n    auto const fastest = n::routing::get_fastest_one_to_all_offsets(\n        tt_, state,\n        query.arriveBy_ ? n::direction::kBackward : n::direction::kForward, l,\n        time, q.max_transfers_);\n\n    all.push_back(api::ReachablePlace{\n        make_place(tt_location{l},\n                   time + std::chrono::minutes{fastest.duration_}, all_ev),\n        query.arriveBy_ ? -fastest.duration_ : fastest.duration_, fastest.k_});\n  });\n\n  return {\n      .one_ = make_place(\n          one, time,\n          query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep),\n      .all_ = std::move(all),\n  };\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/one_to_many.cc",
    "content": "#include \"motis/endpoints/one_to_many.h\"\n\n#include <chrono>\n#include <limits>\n#include <optional>\n\n#include \"utl/enumerate.h\"\n\n#include \"net/too_many_exception.h\"\n\n#include \"nigiri/common/delta_t.h\"\n#include \"nigiri/routing/one_to_all.h\"\n\n#include \"motis/config.h\"\n#include \"motis/endpoints/one_to_many_post.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/gbfs/routing_data.h\"\n#include \"motis/osr/mode_to_profile.h\"\n#include \"motis/timetable/modes_to_clasz_mask.h\"\n\nnamespace motis::ep {\n\nnamespace n = nigiri;\n\nconstexpr auto const kInfinity = std::numeric_limits<double>::infinity();\n\napi::oneToMany_response one_to_many_direct(\n    config const& config,\n    osr::ways const& w,\n    osr::lookup const& l,\n    api::ModeEnum const mode,\n    osr::location const& one,\n    std::vector<osr::location> const& many,\n    double const max_direct_time,\n    double const max_matching_distance,\n    osr::direction const dir,\n    osr_parameters const& params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    osr::elevation_storage const* elevations_,\n    bool const with_distance) {\n  auto const max_many = config.get_limits().onetomany_max_many_;\n  auto const max_direct_time_limit =\n      config.get_limits().street_routing_max_direct_seconds_;\n\n  utl::verify<net::too_many_exception>(\n      many.size() <= max_many,\n      \"number of many locations too high ({} > {}). The server admin can \"\n      \"change this limit in config.yml with 'onetomany_max_many'. \"\n      \"See documentation for details.\",\n      many.size(), max_many);\n  utl::verify<net::too_many_exception>(\n      max_direct_time <= max_direct_time_limit,\n      \"maximun travel time too high ({} > {}). The server admin can \"\n      \"change this limit in config.yml with \"\n      \"'street_routing_max_direct_seconds'. \"\n      \"See documentation for details.\",\n      max_direct_time, max_direct_time_limit);\n  utl::verify<net::bad_request_exception>(\n      mode == api::ModeEnum::BIKE || mode == api::ModeEnum::CAR ||\n          mode == api::ModeEnum::WALK,\n      \"mode {} not supported for one-to-many\", fmt::streamed(mode));\n\n  auto const profile = to_profile(mode, pedestrian_profile, elevation_costs);\n  auto const paths =\n      osr::route(to_profile_parameters(profile, params), w, l, profile, one,\n                 many, max_direct_time, dir, max_matching_distance, nullptr,\n                 nullptr, elevations_, [&](auto&&) { return with_distance; });\n\n  return utl::to_vec(paths, [&](std::optional<osr::path> const& p) {\n    return p\n        .transform([&](osr::path const& x) {\n          return api::Duration{.duration_ = x.cost_,\n                               .distance_ = with_distance\n                                                ? std::optional{x.dist_}\n                                                : std::nullopt};\n        })\n        .value_or(api::Duration{});\n  });\n}\n\ndouble duration_to_seconds(n::duration_t const d) { return 60 * d.count(); }\n\ntemplate <typename Endpoint, typename Query>\nstd::vector<api::ParetoSet> transit_durations(\n    Endpoint const& ep,\n    Query const& query,\n    place_t const& one,\n    std::vector<place_t> const& many,\n    auto const& time,\n    bool const arrive_by,\n    std::chrono::seconds const max_travel_time,\n    double const max_matching_distance,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    osr_parameters const& osr_params) {\n  // Code is similar to One-to-All\n  auto const one_modes =\n      deduplicate(arrive_by ? query.postTransitModes_ : query.preTransitModes_);\n  auto const many_modes =\n      deduplicate(arrive_by ? query.preTransitModes_ : query.postTransitModes_);\n  auto const max_prepost_seconds = std::min(\n      max_travel_time,\n      std::chrono::seconds{\n          ep.config_.get_limits().street_routing_max_prepost_transit_seconds_});\n  auto const one_max_seconds =\n      std::min(std::chrono::seconds{arrive_by ? query.maxPostTransitTime_\n                                              : query.maxPreTransitTime_},\n               max_prepost_seconds);\n  auto const many_max_seconds =\n      std::min(std::chrono::seconds{arrive_by ? query.maxPreTransitTime_\n                                              : query.maxPostTransitTime_},\n               max_prepost_seconds);\n  auto const one_dir =\n      arrive_by ? osr::direction::kBackward : osr::direction::kForward;\n  auto const unreachable = arrive_by ? n::kInvalidDelta<n::direction::kBackward>\n                                     : n::kInvalidDelta<n::direction::kForward>;\n\n  auto const r = routing{ep.config_,     ep.w_,   ep.l_,       ep.pl_,\n                         ep.elevations_, &ep.tt_, nullptr,     &ep.tags_,\n                         ep.loc_tree_,   ep.fa_,  ep.matches_, ep.way_matches_,\n                         ep.rt_,         nullptr, ep.gbfs_,    nullptr,\n                         nullptr,        nullptr, nullptr,     ep.metrics_};\n  auto gbfs_rd = gbfs::gbfs_routing_data{ep.w_, ep.l_, ep.gbfs_};\n\n  auto prepare_stats = std::map<std::string, std::uint64_t>{};\n\n  auto q = n::routing::query{\n      .start_time_ = time,\n      .start_match_mode_ = get_match_mode(r, one),\n      .start_ = r.get_offsets(nullptr, one, one_dir, one_modes, std::nullopt,\n                              std::nullopt, std::nullopt, std::nullopt, false,\n                              osr_params, pedestrian_profile, elevation_costs,\n                              one_max_seconds, max_matching_distance, gbfs_rd,\n                              prepare_stats),\n      .td_start_ = r.get_td_offsets(nullptr, nullptr, one, one_dir, one_modes,\n                                    osr_params, pedestrian_profile,\n                                    elevation_costs, max_matching_distance,\n                                    one_max_seconds, time, prepare_stats),\n      .max_transfers_ = static_cast<std::uint8_t>(\n          query.maxTransfers_.value_or(n::routing::kMaxTransfers)),\n      .max_travel_time_ =\n          std::chrono::duration_cast<n::duration_t>(max_travel_time),\n      .prf_idx_ = query.useRoutedTransfers_\n                      ? (query.pedestrianProfile_ ==\n                                 api::PedestrianProfileEnum::WHEELCHAIR\n                             ? n::kWheelchairProfile\n                             : n::kFootProfile)\n                      : n::kDefaultProfile,\n      .allowed_claszes_ = to_clasz_mask(query.transitModes_),\n      .require_bike_transport_ = query.requireBikeTransport_,\n      .require_car_transport_ = query.requireCarTransport_,\n      .transfer_time_settings_ =\n          n::routing::transfer_time_settings{\n              .default_ = (query.minTransferTime_ == 0 &&\n                           query.additionalTransferTime_ == 0 &&\n                           query.transferTimeFactor_ == 1.0),\n              .min_transfer_time_ = n::duration_t{query.minTransferTime_},\n              .additional_time_ = n::duration_t{query.additionalTransferTime_},\n              .factor_ = static_cast<float>(query.transferTimeFactor_)},\n  };\n\n  if (ep.tt_.locations_.footpaths_out_.at(q.prf_idx_).empty()) {\n    q.prf_idx_ = n::kDefaultProfile;\n  }\n\n  // Compute and update durations using transits\n\n  auto const state =\n      arrive_by\n          ? n::routing::one_to_all<n::direction::kBackward>(ep.tt_, nullptr, q)\n          : n::routing::one_to_all<n::direction::kForward>(ep.tt_, nullptr, q);\n\n  auto reachable = n::bitvec{ep.tt_.n_locations()};\n  for (auto i = 0U; i != ep.tt_.n_locations(); ++i) {\n    if (state.template get_best<0>()[i][0] != unreachable) {\n      reachable.set(i);\n    }\n  }\n\n  auto pareto_sets = std::vector<api::ParetoSet>{};\n\n  auto const dir = arrive_by ? n::direction::kBackward : n::direction::kForward;\n  // As we iterate over all offsets first, we need to store all best solutions\n  // If we would iterate over k first, we could build the pareto set directly.\n  // However, that would require more work for handling internals like `delta_t`\n  auto totals = n::vector<double>{};\n  for (auto const [i, l] : utl::enumerate(many)) {\n    totals.clear();\n    totals.resize(q.max_transfers_, kInfinity);\n    auto const offsets = r.get_offsets(\n        nullptr, l,\n        arrive_by ? osr::direction::kForward : osr::direction::kBackward,\n        many_modes, std::nullopt, std::nullopt, std::nullopt, std::nullopt,\n        false, osr_params, pedestrian_profile, elevation_costs,\n        many_max_seconds, max_matching_distance, gbfs_rd, prepare_stats);\n\n    for (auto const offset : offsets) {\n      auto const loc = offset.target();\n      if (reachable.test(to_idx(loc))) {\n        auto const base = duration_to_seconds(offset.duration());\n        n::routing::for_each_one_to_all_round_time(\n            ep.tt_, state, dir, loc, time, q.max_transfers_,\n            [&](std::uint8_t const k, n::duration_t const d) {\n              if (k != std::uint8_t{0U}) {\n                auto const total = base + duration_to_seconds(d);\n                totals[k - 1U] = std::min(totals[k - 1], total);\n              }\n            });\n      }\n    }\n    auto entries = api::ParetoSet{};\n    auto best = kInfinity;\n    for (auto const [t, d] : utl::enumerate(totals)) {\n      // Filter long durations from offsets with many required transfers\n      if (d < best) {\n        entries.emplace_back(d, t);\n        best = d;\n      }\n    }\n    pareto_sets.emplace_back(std::move(entries));\n  }\n  return pareto_sets;\n}\n\napi::oneToMany_response one_to_many::operator()(\n    boost::urls::url_view const& url) const {\n  auto const query = api::oneToMany_params{url.params()};\n  return one_to_many_handle_request(config_, query, w_, l_, elevations_,\n                                    metrics_);\n}\n\ntemplate <typename Endpoint, typename Query>\napi::OneToManyIntermodalResponse run_one_to_many_intermodal(\n    Endpoint const& ep,\n    Query const& query,\n    place_t const& one,\n    std::vector<place_t> const& many) {\n  ep.metrics_->one_to_many_requests_.Increment();\n  auto const time = std::chrono::time_point_cast<std::chrono::minutes>(\n      *query.time_.value_or(openapi::now()));\n\n  auto const max_travel_time =\n      query.maxTravelTime_\n          .transform([](std::int64_t const dur) {\n            using namespace std::chrono;\n            return duration_cast<seconds>(minutes{dur});\n          })\n          .value_or(n::routing::kMaxTravelTime);\n\n  auto const osr_params = get_osr_parameters(query);\n\n  // Get street routing durations\n  auto const to_location = [&](place_t const& p) {\n    return get_location(&ep.tt_, ep.w_, ep.pl_, ep.matches_, p);\n  };\n  return {.street_durations_ = one_to_many_direct(\n              ep.config_, *ep.w_, *ep.l_, query.directMode_, to_location(one),\n              utl::to_vec(many, to_location),\n              static_cast<double>(std::min(\n                  {std::max({query.maxDirectTime_, query.maxPreTransitTime_,\n                             query.maxPostTransitTime_}),\n                   static_cast<std::int64_t>(max_travel_time.count()),\n                   static_cast<std::int64_t>(\n                       ep.config_.get_limits()\n                           .street_routing_max_direct_seconds_)})),\n              query.maxMatchingDistance_,\n              query.arriveBy_ ? osr::direction::kBackward\n                              : osr::direction::kForward,\n              osr_params, query.pedestrianProfile_, query.elevationCosts_,\n              ep.elevations_, query.withDistance_),\n          .transit_durations_ = transit_durations(\n              ep, query, one, many, time, query.arriveBy_, max_travel_time,\n              query.maxMatchingDistance_, query.pedestrianProfile_,\n              query.elevationCosts_, osr_params)};\n}\n\napi::OneToManyIntermodalResponse one_to_many_intermodal::operator()(\n    boost::urls::url_view const& url) const {\n  auto const query = api::oneToManyIntermodal_params{url.params()};\n\n  auto const one = parse_location(query.one_, ';');\n  utl::verify<net::bad_request_exception>(\n      one.has_value(), \"{} is not a valid geo coordinate\", query.one_);\n\n  auto const many = utl::to_vec(query.many_, [](auto&& x) -> place_t {\n    auto const y = parse_location(x, ';');\n    utl::verify<net::bad_request_exception>(\n        y.has_value(), \"{} is not a valid geo coordinate\", x);\n    return *y;\n  });\n\n  return run_one_to_many_intermodal(*this, query, *one, many);\n}\n\ntemplate api::OneToManyIntermodalResponse run_one_to_many_intermodal<\n    one_to_many_intermodal,\n    api::oneToManyIntermodal_params>(one_to_many_intermodal const&,\n                                     api::oneToManyIntermodal_params const&,\n                                     place_t const&,\n                                     std::vector<place_t> const&);\n\ntemplate api::OneToManyIntermodalResponse run_one_to_many_intermodal<\n    one_to_many_intermodal_post,\n    api::OneToManyIntermodalParams>(one_to_many_intermodal_post const&,\n                                    api::OneToManyIntermodalParams const&,\n                                    place_t const&,\n                                    std::vector<place_t> const&);\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/one_to_many_post.cc",
    "content": "#include \"motis/endpoints/one_to_many_post.h\"\n\n#include <string_view>\n\n#include \"utl/to_vec.h\"\n\n#include \"motis/endpoints/one_to_many.h\"\n#include \"motis/place.h\"\n\nnamespace motis::ep {\n\napi::oneToManyPost_response one_to_many_post::operator()(\n    api::OneToManyParams const& query) const {\n  return one_to_many_handle_request(config_, query, w_, l_, elevations_,\n                                    metrics_);\n}\n\napi::OneToManyIntermodalResponse one_to_many_intermodal_post::operator()(\n    api::OneToManyIntermodalParams const& query) const {\n  auto const one = get_place(&tt_, &tags_, query.one_);\n  auto const many =\n      utl::to_vec(query.many_, [&](std::string_view place) -> place_t {\n        return get_place(&tt_, &tags_, place);\n      });\n  return run_one_to_many_intermodal(*this, query, one, many);\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/osr_routing.cc",
    "content": "#include \"motis/endpoints/osr_routing.h\"\n\n#include \"utl/pipes.h\"\n\n#include \"osr/geojson.h\"\n#include \"osr/routing/route.h\"\n\n#include \"motis/data.h\"\n#include \"motis/osr/parameters.h\"\n\nnamespace json = boost::json;\n\nnamespace motis::ep {\n\nosr::location parse_location(json::value const& v) {\n  auto const& obj = v.as_object();\n  return {{obj.at(\"lat\").as_double(), obj.at(\"lng\").as_double()},\n          obj.contains(\"level\")\n              ? osr::level_t{obj.at(\"level\").to_number<float>()}\n              : osr::kNoLevel};\n}\n\njson::value osr_routing::operator()(json::value const& query) const {\n  auto const rt = std::atomic_load(&rt_);\n  auto const e = rt->e_.get();\n\n  auto const& q = query.as_object();\n  auto const profile_it = q.find(\"profile\");\n  auto const profile =\n      osr::to_profile(profile_it == q.end() || !profile_it->value().is_string()\n                          ? to_str(osr::search_profile::kFoot)\n                          : profile_it->value().as_string());\n  auto const direction_it = q.find(\"direction\");\n  auto const dir = osr::to_direction(direction_it == q.end() ||\n                                             !direction_it->value().is_string()\n                                         ? to_str(osr::direction::kForward)\n                                         : direction_it->value().as_string());\n  auto const from = parse_location(q.at(\"start\"));\n  auto const to = parse_location(q.at(\"destination\"));\n  auto const max_it = q.find(\"max\");\n  auto const max = static_cast<osr::cost_t>(\n      max_it == q.end() ? 3600 : max_it->value().as_int64());\n  auto const p =\n      route(to_profile_parameters(profile, {}), w_, l_, profile, from, to, max,\n            dir, 8, e == nullptr ? nullptr : &e->blocked_);\n  return p.has_value()\n             ? json::value{{\"type\", \"FeatureCollection\"},\n                           {\"metadata\",\n                            {{\"duration\", p->cost_},\n                             {\"distance\", p->dist_},\n                             {\"uses_elevator\", p->uses_elevator_}}},\n                           {\"features\",\n                            utl::all(p->segments_)  //\n                                |\n                                utl::transform([&](auto&& s) {\n                                  return json::value{\n                                      {\"type\", \"Feature\"},\n                                      {\"properties\",\n                                       {{\"level\", s.from_level_.to_float()},\n                                        {\"way\",\n                                         s.way_ == osr::way_idx_t::invalid()\n                                             ? 0U\n                                             : to_idx(\n                                                   w_.way_osm_idx_[s.way_])}}},\n                                      {\"geometry\",\n                                       osr::to_line_string(s.polyline_)}};\n                                })  //\n                                | utl::emplace_back_to<json::array>()}}\n             : json::value{{\"error\", \"no path found\"}};\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/platforms.cc",
    "content": "#include \"motis/endpoints/platforms.h\"\n\n#include \"net/too_many_exception.h\"\n\n#include \"osr/geojson.h\"\n\nnamespace json = boost::json;\n\nnamespace motis::ep {\n\nconstexpr auto const kLimit = 4096U;\n\njson::value platforms::operator()(json::value const& query) const {\n  auto const& q = query.as_object();\n  auto const level = q.contains(\"level\")\n                         ? osr::level_t{query.at(\"level\").to_number<float>()}\n                         : osr::kNoLevel;\n  auto const waypoints = q.at(\"waypoints\").as_array();\n  auto const min = osr::point::from_latlng(\n      {waypoints[1].as_double(), waypoints[0].as_double()});\n  auto const max = osr::point::from_latlng(\n      {waypoints[3].as_double(), waypoints[2].as_double()});\n\n  auto gj = osr::geojson_writer{.w_ = w_, .platforms_ = &pl_};\n  pl_.find(min, max, [&](osr::platform_idx_t const i) {\n    if (level == osr::kNoLevel || pl_.get_level(w_, i) == level) {\n      utl::verify<net::too_many_exception>(gj.features_.size() < kLimit,\n                                           \"too many platforms\");\n      gj.write_platform(i);\n    }\n  });\n\n  return gj.json();\n}\n\n}  // namespace motis::ep"
  },
  {
    "path": "src/endpoints/routing.cc",
    "content": "#include \"motis/endpoints/routing.h\"\n\n#include <algorithm>\n#include <cmath>\n\n#include \"boost/thread/tss.hpp\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/too_many_exception.h\"\n\n#include \"prometheus/counter.h\"\n#include \"prometheus/histogram.h\"\n\n#include \"utl/erase_duplicates.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/timing.h\"\n\n#include \"osr/lookup.h\"\n#include \"osr/platforms.h\"\n#include \"osr/routing/profile.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/routing/sharing_data.h\"\n#include \"osr/types.h\"\n\n#include \"nigiri/common/interval.h\"\n#include \"nigiri/location_match_mode.h\"\n#include \"nigiri/routing/limits.h\"\n#include \"nigiri/routing/pareto_set.h\"\n#include \"nigiri/routing/query.h\"\n#include \"nigiri/routing/raptor/raptor_state.h\"\n#include \"nigiri/routing/raptor_search.h\"\n\n#include \"nigiri/routing/raptor/pong.h\"\n#include \"nigiri/routing/tb/query_engine.h\"\n#include \"nigiri/routing/tb/tb_data.h\"\n#include \"nigiri/routing/tb/tb_search.h\"\n\n#include \"motis/config.h\"\n#include \"motis/constants.h\"\n#include \"motis/direct_filter.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/flex/flex.h\"\n#include \"motis/flex/flex_output.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/gbfs/gbfs_output.h\"\n#include \"motis/gbfs/mode.h\"\n#include \"motis/gbfs/osr_profile.h\"\n#include \"motis/get_stops_with_traffic.h\"\n#include \"motis/journey_to_response.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/metrics_registry.h\"\n#include \"motis/odm/meta_router.h\"\n#include \"motis/osr/max_distance.h\"\n#include \"motis/osr/mode_to_profile.h\"\n#include \"motis/osr/street_routing.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/server.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/modes_to_clasz_mask.h\"\n#include \"motis/timetable/time_conv.h\"\n#include \"motis/update_rtt_td_footpaths.h\"\n\nnamespace n = nigiri;\nusing namespace std::chrono_literals;\n\nnamespace motis::ep {\n\n// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)\nboost::thread_specific_ptr<osr::bitvec<osr::node_idx_t>> blocked;\n\nbool osr_loaded(routing const& r) {\n  return r.w_ && r.l_ && r.pl_ && r.tt_ && r.loc_tree_ && r.matches_;\n}\n\nbool is_intermodal(routing const& r, place_t const&) {\n  return osr_loaded(r);  // use pre/post transit even when start/dest is a stop\n}\n\nn::routing::location_match_mode get_match_mode(routing const& r,\n                                               place_t const& p) {\n  return is_intermodal(r, p) ? n::routing::location_match_mode::kIntermodal\n                             : n::routing::location_match_mode::kEquivalent;\n}\n\nstd::vector<n::routing::offset> station_start(n::location_idx_t const l) {\n  return {{l, n::duration_t{0U}, 0U}};\n}\n\nstd::vector<n::routing::offset> radius_offsets(\n    point_rtree<n::location_idx_t> const& loc_tree,\n    geo::latlng const& pos,\n    double const radius_meters) {\n  auto offsets = std::vector<n::routing::offset>{};\n  loc_tree.in_radius(pos, radius_meters, [&](n::location_idx_t const l) {\n    offsets.push_back({l, n::duration_t{0U}, 0U});\n  });\n  return offsets;\n}\n\nosr::location stop_to_osr_location(routing const& r,\n                                   n::location_idx_t const l) {\n  return osr::location{r.tt_->locations_.coordinates_[l],\n                       r.pl_->get_level(*r.w_, (*r.matches_)[l])};\n}\n\nn::routing::td_offsets_t get_td_offsets(\n    routing const& r,\n    n::rt_timetable const* rtt,\n    elevators const* e,\n    osr::location const& pos,\n    osr::direction const dir,\n    std::vector<api::ModeEnum> const& modes,\n    osr_parameters const& osr_params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    double const max_matching_distance,\n    std::chrono::seconds const max,\n    nigiri::routing::start_time_t const& start_time,\n    stats_map_t& stats) {\n  if (!osr_loaded(r)) {\n    return {};\n  }\n\n  auto ret = hash_map<n::location_idx_t, std::vector<n::routing::td_offset>>{};\n  for (auto const m : modes) {\n    if (m == api::ModeEnum::ODM || m == api::ModeEnum::RIDE_SHARING) {\n      continue;\n    } else if (m == api::ModeEnum::FLEX) {\n      UTL_START_TIMING(flex_timer);\n      utl::verify(r.fa_, \"FLEX areas not loaded\");\n      auto frd = flex::flex_routing_data{};\n      flex::add_flex_td_offsets(*r.w_, *r.l_, r.pl_, r.matches_, r.way_matches_,\n                                *r.tt_, *r.fa_, *r.loc_tree_, start_time, pos,\n                                dir, max, max_matching_distance, osr_params,\n                                frd, ret, stats);\n      stats.emplace(fmt::format(\"prepare_{}_FLEX\", to_str(dir)),\n                    UTL_GET_TIMING_MS(flex_timer));\n      continue;\n    }\n\n    auto const profile = to_profile(m, pedestrian_profile, elevation_costs);\n\n    if (e == nullptr || profile != osr::search_profile::kWheelchair) {\n      continue;  // handled by get_offsets\n    }\n\n    utl::equal_ranges_linear(\n        get_td_footpaths(*r.w_, *r.l_, *r.pl_, *r.tt_, rtt, *r.loc_tree_, *e,\n                         *r.matches_, n::location_idx_t::invalid(), pos, dir,\n                         profile, max, max_matching_distance, osr_params,\n                         *blocked),\n        [](n::td_footpath const& a, n::td_footpath const& b) {\n          return a.target_ == b.target_;\n        },\n        [&](auto&& from, auto&& to) {\n          ret.emplace(from->target_,\n                      utl::to_vec(from, to, [&](n::td_footpath const fp) {\n                        return n::routing::td_offset{\n                            .valid_from_ = fp.valid_from_,\n                            .duration_ = fp.duration_,\n                            .transport_mode_id_ =\n                                static_cast<n::transport_mode_id_t>(profile)};\n                      }));\n        });\n  }\n\n  return ret;\n}\n\nn::routing::td_offsets_t routing::get_td_offsets(\n    n::rt_timetable const* rtt,\n    elevators const* e,\n    place_t const& p,\n    osr::direction const dir,\n    std::vector<api::ModeEnum> const& modes,\n    osr_parameters const& osr_params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    double const max_matching_distance,\n    std::chrono::seconds const max,\n    nigiri::routing::start_time_t const& start_time,\n    stats_map_t& stats) const {\n  return std::visit(\n      utl::overloaded{\n          [&](tt_location l) {\n            if (!osr_loaded(*this)) {\n              return n::routing::td_offsets_t{};\n            }\n            return ::motis::ep::get_td_offsets(\n                *this, rtt, e, stop_to_osr_location(*this, l.l_), dir, modes,\n                osr_params, pedestrian_profile, elevation_costs,\n                max_matching_distance, max, start_time, stats);\n          },\n          [&](osr::location const& pos) {\n            return ::motis::ep::get_td_offsets(\n                *this, rtt, e, pos, dir, modes, osr_params, pedestrian_profile,\n                elevation_costs, max_matching_distance, max, start_time, stats);\n          }},\n      p);\n}\n\nbool include_rental_provider(\n    std::optional<std::vector<std::string>> const& rental_providers,\n    std::optional<std::vector<std::string>> const& rental_provider_groups,\n    gbfs::gbfs_provider const* provider) {\n  if (provider == nullptr) {\n    return false;\n  }\n  if ((!rental_providers || rental_providers->empty()) &&\n      (!rental_provider_groups || rental_provider_groups->empty())) {\n    return true;\n  }\n  return (rental_provider_groups &&\n          utl::find(*rental_provider_groups, provider->group_id_) !=\n              end(*rental_provider_groups)) ||\n         (rental_providers && utl::find(*rental_providers, provider->id_) !=\n                                  end(*rental_providers));\n}\n\nstd::vector<n::routing::offset> get_offsets(\n    routing const& r,\n    n::rt_timetable const* rtt,\n    osr::location const& pos,\n    osr::direction const dir,\n    osr::elevation_storage const* elevations,\n    std::vector<api::ModeEnum> const& modes,\n    std::optional<std::vector<api::RentalFormFactorEnum>> const& form_factors,\n    std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n        propulsion_types,\n    std::optional<std::vector<std::string>> const& rental_providers,\n    std::optional<std::vector<std::string>> const& rental_provider_groups,\n    bool const ignore_rental_return_constraints,\n    osr_parameters const& osr_params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    std::chrono::seconds const max,\n    double const max_matching_distance,\n    gbfs::gbfs_routing_data& gbfs_rd,\n    stats_map_t& stats) {\n  if (!osr_loaded(r)) {\n    return {};\n  }\n  auto offsets = std::vector<n::routing::offset>{};\n  auto ignore_walk = false;\n\n  auto const handle_mode = [&](api::ModeEnum const m) {\n    UTL_START_TIMING(timer);\n\n    auto profile = to_profile(m, pedestrian_profile, elevation_costs);\n\n    if (auto const rt = std::atomic_load(&r.rt_);\n        rt->e_ && profile == osr::search_profile::kWheelchair) {\n      return;  // handled by get_td_offsets\n    }\n\n    if (osr::is_rental_profile(profile) &&\n        (!form_factors.has_value() ||\n         utl::any_of(*form_factors, [](auto const f) {\n           return gbfs::get_osr_profile(gbfs::from_api_form_factor(f)) ==\n                  osr::search_profile::kCarSharing;\n         }))) {\n      profile = osr::search_profile::kCarSharing;\n    }\n\n    auto const max_dist = get_max_distance(profile, max);\n    auto const near_stops =\n        get_stops_with_traffic(*r.tt_, rtt, *r.loc_tree_, pos, max_dist);\n    auto const near_stop_locations = utl::to_vec(\n        near_stops,\n        [&](n::location_idx_t const l) { return stop_to_osr_location(r, l); });\n\n    auto const route = [&](osr::search_profile const p,\n                           osr::sharing_data const* sharing) {\n      auto const params = to_profile_parameters(p, osr_params);\n      auto const pos_match = r.l_->match(params, pos, false, dir,\n                                         max_matching_distance, nullptr, p);\n      auto const near_stop_matches = get_reverse_platform_way_matches(\n          *r.l_, r.way_matches_, p, near_stops, near_stop_locations, dir,\n          max_matching_distance);\n      return osr::route(params, *r.w_, *r.l_, p, pos, near_stop_locations,\n                        pos_match, near_stop_matches,\n                        static_cast<osr::cost_t>(max.count()), dir, nullptr,\n                        sharing, elevations);\n    };\n\n    if (osr::is_rental_profile(profile)) {\n      if (!gbfs_rd.has_data()) {\n        return;\n      }\n\n      auto const max_dist_to_departure =\n          dir == osr::direction::kForward\n              ? get_max_distance(osr::search_profile::kFoot, max)\n              : max_dist;\n      auto providers = hash_set<gbfs_provider_idx_t>{};\n      gbfs_rd.data_->provider_rtree_.in_radius(\n          pos.pos_, max_dist_to_departure,\n          [&](auto const pi) { providers.insert(pi); });\n\n      for (auto const& pi : providers) {\n        UTL_START_TIMING(provider_timer);\n\n        auto const& provider = gbfs_rd.data_->providers_.at(pi);\n        if (!include_rental_provider(rental_providers, rental_provider_groups,\n                                     provider.get())) {\n          continue;\n        }\n        auto provider_rd = std::shared_ptr<gbfs::provider_routing_data>{};\n        for (auto const& prod : provider->products_) {\n          if ((prod.return_constraint_ ==\n                   gbfs::return_constraint::kRoundtripStation &&\n               !ignore_rental_return_constraints) ||\n              !gbfs::products_match(prod, form_factors, propulsion_types)) {\n            continue;\n          }\n          if (!provider_rd) {\n            provider_rd = gbfs_rd.get_provider_routing_data(*provider);\n          }\n          auto const prod_ref = gbfs::gbfs_products_ref{pi, prod.idx_};\n          auto* prod_rd =\n              gbfs_rd.get_products_routing_data(*provider, prod.idx_);\n          auto const sharing = prod_rd->get_sharing_data(\n              r.w_->n_nodes(), ignore_rental_return_constraints);\n\n          auto const paths =\n              route(gbfs::get_osr_profile(prod.form_factor_), &sharing);\n          ignore_walk = true;\n          for (auto const [p, l] : utl::zip(paths, near_stops)) {\n            if (p.has_value()) {\n              offsets.emplace_back(l,\n                                   n::duration_t{static_cast<unsigned>(\n                                       std::ceil(p->cost_ / 60.0))},\n                                   gbfs_rd.get_transport_mode(prod_ref));\n            }\n          }\n        }\n\n        stats.emplace(fmt::format(\"prepare_{}_{}_{}\", to_str(dir),\n                                  fmt::streamed(m), provider->id_),\n                      UTL_GET_TIMING_MS(provider_timer));\n      }\n\n    } else {\n      auto const paths = route(profile, nullptr);\n      for (auto const [p, l] : utl::zip(paths, near_stops)) {\n        if (p.has_value()) {\n          offsets.emplace_back(\n              l,\n              n::duration_t{static_cast<unsigned>(std::ceil(p->cost_ / 60.0))},\n              static_cast<n::transport_mode_id_t>(profile));\n        }\n      }\n    }\n\n    stats.emplace(fmt::format(\"prepare_{}_{}\", to_str(dir), fmt::streamed(m)),\n                  UTL_GET_TIMING_MS(timer));\n  };\n\n  if (utl::find(modes, api::ModeEnum::RENTAL) != end(modes)) {\n    handle_mode(api::ModeEnum::RENTAL);\n  }\n\n  for (auto const m : modes) {\n    if (m == api::ModeEnum::RENTAL || m == api::ModeEnum::FLEX ||\n        (m == api::ModeEnum::WALK && ignore_walk)) {\n      continue;\n    }\n    handle_mode(m);\n  }\n\n  return offsets;\n}\n\nn::interval<n::unixtime_t> shrink(bool const keep_late,\n                                  std::size_t const max_size,\n                                  n::interval<n::unixtime_t> search_interval,\n                                  std::vector<n::routing::journey>& journeys) {\n  if (journeys.size() <= max_size) {\n    return search_interval;\n  }\n\n  if (keep_late) {\n    auto cutoff_it =\n        std::next(journeys.rbegin(), static_cast<int>(max_size - 1));\n    auto last_arr_time = cutoff_it->start_time_;\n    ++cutoff_it;\n    while (cutoff_it != rend(journeys) &&\n           cutoff_it->start_time_ == last_arr_time) {\n      ++cutoff_it;\n    }\n    if (cutoff_it == rend(journeys)) {\n      return search_interval;\n    }\n    search_interval.from_ = cutoff_it->start_time_ + std::chrono::minutes{1};\n    journeys.erase(begin(journeys), cutoff_it.base());\n  } else {\n    auto cutoff_it = std::next(begin(journeys), static_cast<int>(max_size - 1));\n    auto last_dep_time = cutoff_it->start_time_;\n    while (cutoff_it != end(journeys) &&\n           cutoff_it->start_time_ == last_dep_time) {\n      ++cutoff_it;\n    }\n    if (cutoff_it == end(journeys)) {\n      return search_interval;\n    }\n    search_interval.to_ = cutoff_it->start_time_;\n    journeys.erase(cutoff_it, end(journeys));\n  }\n\n  return search_interval;\n}\n\nstd::vector<n::routing::offset> routing::get_offsets(\n    n::rt_timetable const* rtt,\n    place_t const& p,\n    osr::direction const dir,\n    std::vector<api::ModeEnum> const& modes,\n    std::optional<std::vector<api::RentalFormFactorEnum>> const& form_factors,\n    std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n        propulsion_types,\n    std::optional<std::vector<std::string>> const& rental_providers,\n    std::optional<std::vector<std::string>> const& rental_provider_groups,\n    bool const ignore_rental_return_constraints,\n    osr_parameters const& osr_params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    std::chrono::seconds const max,\n    double const max_matching_distance,\n    gbfs::gbfs_routing_data& gbfs_rd,\n    stats_map_t& stats) const {\n  auto const do_get_offsets = [&](osr::location const pos) {\n    return ::motis::ep::get_offsets(\n        *this, rtt, pos, dir, elevations_, modes, form_factors,\n        propulsion_types, rental_providers, rental_provider_groups,\n        ignore_rental_return_constraints, osr_params, pedestrian_profile,\n        elevation_costs, max, max_matching_distance, gbfs_rd, stats);\n  };\n  return std::visit(\n      utl::overloaded{\n          [&](tt_location const l) {\n            if (!osr_loaded(*this)) {\n              return station_start(l.l_);\n            }\n            auto offsets = do_get_offsets(stop_to_osr_location(*this, l.l_));\n            for_each_meta(*tt_,\n                          nigiri::routing::location_match_mode::kEquivalent,\n                          l.l_, [&](n::location_idx_t const c) {\n                            offsets.emplace_back(c, n::duration_t{0U}, 0U);\n                          });\n\n            return offsets;\n          },\n          [&](osr::location const& pos) { return do_get_offsets(pos); }},\n      p);\n}\n\nstd::pair<n::routing::query, std::optional<n::unixtime_t>> get_start_time(\n    api::plan_params const& query, nigiri::timetable const* tt) {\n  if (query.pageCursor_.has_value()) {\n    return {cursor_to_query(*query.pageCursor_), std::nullopt};\n  } else {\n    auto const t = std::chrono::time_point_cast<n::i32_minutes>(\n        *query.time_.value_or(openapi::now()));\n    utl::verify<net::bad_request_exception>(\n        tt == nullptr || tt->external_interval().contains(t),\n        \"query time {} is outside of loaded timetable window {}\", t,\n        tt ? tt->external_interval() : n::interval<n::unixtime_t>{});\n    auto const window =\n        std::chrono::duration_cast<n::duration_t>(std::chrono::seconds{\n            query.searchWindow_ *\n            (query.arriveBy_ ? -1 : 1)});  // TODO redundant minus\n    return {{.start_time_ = query.timetableView_ && tt\n                                ? n::routing::start_time_t{n::interval{\n                                      tt->external_interval().clamp(\n                                          query.arriveBy_ ? t - window : t),\n                                      tt->external_interval().clamp(\n                                          query.arriveBy_ ? t + n::duration_t{1}\n                                                          : t + window)}}\n                                : n::routing::start_time_t{t},\n             .extend_interval_earlier_ = query.arriveBy_,\n             .extend_interval_later_ = !query.arriveBy_},\n            t};\n  }\n}\n\nstd::pair<std::vector<api::Itinerary>, n::duration_t> routing::route_direct(\n    elevators const* e,\n    gbfs::gbfs_routing_data& gbfs_rd,\n    n::lang_t const& lang,\n    api::Place const& from,\n    api::Place const& to,\n    std::vector<api::ModeEnum> const& modes,\n    std::optional<std::vector<api::RentalFormFactorEnum>> const& form_factors,\n    std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n        propulsion_types,\n    std::optional<std::vector<std::string>> const& rental_providers,\n    std::optional<std::vector<std::string>> const& rental_provider_groups,\n    bool const ignore_rental_return_constraints,\n    n::unixtime_t const time,\n    bool const arrive_by,\n    osr_parameters const& osr_params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    std::chrono::seconds max,\n    double const max_matching_distance,\n    double const fastest_direct_factor,\n    bool const detailed_legs,\n    unsigned const api_version) const {\n  if (!w_ || !l_) {\n    return {};\n  }\n  auto fastest_direct = kInfinityDuration;\n  auto cache = street_routing_cache_t{};\n  auto itineraries = std::vector<api::Itinerary>{};\n\n  auto const route_with_profile = [&](output const& out) {\n    auto itinerary = street_routing(\n        *w_, *l_, e, elevations_, lang, from, to, out,\n        arrive_by ? std::nullopt : std::optional{time},\n        arrive_by ? std::optional{time} : std::nullopt, max_matching_distance,\n        osr_params, cache, *blocked, api_version, detailed_legs, max);\n    if (itinerary.legs_.empty()) {\n      return false;\n    }\n    auto const duration = std::chrono::duration_cast<n::duration_t>(\n        std::chrono::seconds{itinerary.duration_});\n    if (duration < fastest_direct) {\n      fastest_direct = duration;\n    }\n    itineraries.emplace_back(std::move(itinerary));\n    return true;\n  };\n\n  for (auto const& m : modes) {\n    if (m == api::ModeEnum::FLEX) {\n      utl::verify(tt_ && tags_ && fa_, \"FLEX requires timetable\");\n      auto const routings = flex::get_flex_routings(\n          *tt_, *loc_tree_, time, get_location(from).pos_,\n          osr::direction::kForward, max);\n      for (auto const& [_, ids] : routings) {\n        route_with_profile(flex::flex_output{*w_, *l_, pl_, matches_, ae_, tz_,\n                                             *tags_, *tt_, *fa_, ids.front()});\n      }\n    } else if (m == api::ModeEnum::CAR || m == api::ModeEnum::BIKE ||\n               m == api::ModeEnum::CAR_PARKING ||\n               m == api::ModeEnum::CAR_DROPOFF ||\n               m == api::ModeEnum::DEBUG_BUS_ROUTE ||\n               m == api::ModeEnum::DEBUG_RAILWAY_ROUTE ||\n               m == api::ModeEnum::DEBUG_FERRY_ROUTE ||\n               m == api::ModeEnum::WALK) {\n      route_with_profile(default_output{\n          *w_, to_profile(m, pedestrian_profile, elevation_costs)});\n    } else if (m == api::ModeEnum::RENTAL && gbfs_rd.has_data()) {\n      // use foot because this is always forward search and we need to walk to\n      // the station/vehicle\n      auto const max_dist = get_max_distance(osr::search_profile::kFoot, max);\n      auto providers = hash_set<gbfs_provider_idx_t>{};\n      auto routed = 0U;\n      gbfs_rd.data_->provider_rtree_.in_radius(\n          {from.lat_, from.lon_}, max_dist,\n          [&](auto const pi) { providers.insert(pi); });\n      for (auto const& pi : providers) {\n        auto const& provider = gbfs_rd.data_->providers_.at(pi);\n        if (!include_rental_provider(rental_providers, rental_provider_groups,\n                                     provider.get())) {\n          continue;\n        }\n        for (auto const& prod : provider->products_) {\n          if (!gbfs::products_match(prod, form_factors, propulsion_types)) {\n            continue;\n          }\n          route_with_profile(gbfs::gbfs_output{\n              *w_, gbfs_rd, gbfs::gbfs_products_ref{provider->idx_, prod.idx_},\n              ignore_rental_return_constraints});\n          ++routed;\n        }\n      }\n      // if we omitted the WALK routing but didn't have any rental providers\n      // in the area, we need to do WALK routing now\n      if (routed == 0U && utl::find(modes, api::ModeEnum::WALK) != end(modes)) {\n        route_with_profile(default_output{\n            *w_, to_profile(api::ModeEnum::WALK, pedestrian_profile,\n                            elevation_costs)});\n      }\n    }\n  }\n  utl::erase_duplicates(itineraries);\n  return {itineraries, fastest_direct != kInfinityDuration\n                           ? std::chrono::round<n::duration_t>(\n                                 fastest_direct * fastest_direct_factor)\n                           : fastest_direct};\n}\n\nusing stats_map_t = std::map<std::string, std::uint64_t>;\n\nstats_map_t join(auto&&... maps) {\n  auto ret = std::map<std::string, std::uint64_t>{};\n  auto const add = [&](std::map<std::string, std::uint64_t> const& x) {\n    ret.insert(begin(x), end(x));\n  };\n  (add(maps), ...);\n  return ret;\n}\n\nvoid remove_slower_than_fastest_direct(n::routing::query& q) {\n  if (!q.fastest_direct_) {\n    return;\n  }\n\n  constexpr auto const kMaxDuration =\n      n::duration_t{std::numeric_limits<n::duration_t::rep>::max()};\n\n  auto const worse_than_fastest_direct = [&](n::duration_t const min) {\n    return [&, min](auto const& o) {\n      return o.duration() < nigiri::footpath::kMaxDuration &&\n             o.duration() + min >= q.fastest_direct_;\n    };\n  };\n  auto const get_min_duration = [&](auto&& x) {\n    return x.empty() ? kMaxDuration\n                     : utl::min_element(x, [](auto&& a, auto&& b) {\n                         return a.duration() < b.duration();\n                       })->duration();\n  };\n\n  auto min_start = get_min_duration(q.start_);\n  for (auto const& [_, v] : q.td_start_) {\n    min_start = std::min(min_start, get_min_duration(v));\n  }\n\n  auto min_dest = get_min_duration(q.destination_);\n  for (auto const& [_, v] : q.td_dest_) {\n    min_dest = std::min(min_dest, get_min_duration(v));\n  }\n\n  utl::erase_if(q.start_, worse_than_fastest_direct(min_dest));\n  utl::erase_if(q.destination_, worse_than_fastest_direct(min_start));\n  for (auto& [k, v] : q.td_start_) {\n    utl::erase_if(v, worse_than_fastest_direct(min_dest));\n  }\n  for (auto& [k, v] : q.td_dest_) {\n    utl::erase_if(v, worse_than_fastest_direct(min_start));\n  }\n}\n\nstd::vector<n::routing::via_stop> get_via_stops(\n    n::timetable const& tt,\n    tag_lookup const& tags,\n    std::optional<std::vector<std::string>> const& vias,\n    std::vector<std::int64_t> const& times,\n    bool const reverse) {\n  if (!vias.has_value()) {\n    return {};\n  }\n\n  auto ret = std::vector<n::routing::via_stop>{};\n  for (auto i = 0U; i != vias->size(); ++i) {\n    ret.push_back({tags.get_location(tt, (*vias)[i]),\n                   n::duration_t{i < times.size() ? times[i] : 0}});\n  }\n\n  if (reverse) {\n    std::reverse(begin(ret), end(ret));\n  }\n  return ret;\n}\n\nstd::vector<api::ModeEnum> deduplicate(std::vector<api::ModeEnum> m) {\n  utl::erase_duplicates(m);\n  return m;\n};\n\napi::plan_response routing::operator()(boost::urls::url_view const& url) const {\n  metrics_->routing_requests_.Increment();\n\n  auto const query = api::plan_params{url.params()};\n  utl::verify<net::bad_request_exception>(\n      !query.maxItineraries_.has_value() ||\n          (*query.maxItineraries_ >= 1 &&\n           *query.maxItineraries_ >= query.numItineraries_),\n      \"maxItineraries={} < numItineraries={}\",\n      query.maxItineraries_.value_or(0), query.numItineraries_);\n\n  auto const rt = std::atomic_load(&rt_);\n  auto const rtt = rt->rtt_.get();\n  auto const e = rt->e_.get();\n  auto gbfs_rd = gbfs::gbfs_routing_data{w_, l_, gbfs_};\n  if (blocked.get() == nullptr && w_ != nullptr) {\n    blocked.reset(new osr::bitvec<osr::node_idx_t>{w_->n_nodes()});\n  }\n\n  auto const api_version = get_api_version(url);\n\n  auto const deduplicate = [](auto m) {\n    utl::erase_duplicates(m);\n    return m;\n  };\n  auto const& lang = query.language_;\n  auto const pre_transit_modes = deduplicate(query.preTransitModes_);\n  auto const post_transit_modes = deduplicate(query.postTransitModes_);\n  auto const direct_modes = deduplicate(query.directModes_);\n  auto const from = get_place(tt_, tags_, query.fromPlace_);\n  auto const to = get_place(tt_, tags_, query.toPlace_);\n  auto from_p = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, from);\n  auto to_p = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, to);\n  if (from_p.vertexType_ == api::VertexTypeEnum::NORMAL) {\n    from_p.name_ = \"START\";\n  }\n  if (to_p.vertexType_ == api::VertexTypeEnum::NORMAL) {\n    to_p.name_ = \"END\";\n  }\n\n  auto const& start = query.arriveBy_ ? to : from;\n  auto const& dest = query.arriveBy_ ? from : to;\n  auto const& start_modes =\n      query.arriveBy_ ? post_transit_modes : pre_transit_modes;\n  auto const& dest_modes =\n      query.arriveBy_ ? pre_transit_modes : post_transit_modes;\n  auto const& start_form_factors = query.arriveBy_\n                                       ? query.postTransitRentalFormFactors_\n                                       : query.preTransitRentalFormFactors_;\n  auto const& dest_form_factors = query.arriveBy_\n                                      ? query.preTransitRentalFormFactors_\n                                      : query.postTransitRentalFormFactors_;\n  auto const& start_propulsion_types =\n      query.arriveBy_ ? query.postTransitRentalPropulsionTypes_\n                      : query.preTransitRentalPropulsionTypes_;\n  auto const& dest_propulsion_types =\n      query.arriveBy_ ? query.postTransitRentalPropulsionTypes_\n                      : query.preTransitRentalPropulsionTypes_;\n  auto const& start_rental_providers = query.arriveBy_\n                                           ? query.postTransitRentalProviders_\n                                           : query.preTransitRentalProviders_;\n  auto const& dest_rental_providers = query.arriveBy_\n                                          ? query.preTransitRentalProviders_\n                                          : query.postTransitRentalProviders_;\n  auto const& start_rental_provider_groups =\n      query.arriveBy_ ? query.postTransitRentalProviderGroups_\n                      : query.preTransitRentalProviderGroups_;\n  auto const& dest_rental_provider_groups =\n      query.arriveBy_ ? query.preTransitRentalProviderGroups_\n                      : query.postTransitRentalProviderGroups_;\n  auto const start_ignore_return_constraints =\n      query.arriveBy_ ? query.ignorePostTransitRentalReturnConstraints_\n                      : query.ignorePreTransitRentalReturnConstraints_;\n  auto const dest_ignore_return_constraints =\n      query.arriveBy_ ? query.ignorePreTransitRentalReturnConstraints_\n                      : query.ignorePostTransitRentalReturnConstraints_;\n  utl::verify<net::too_many_exception>(\n      query.searchWindow_ / 60 <\n          config_.get_limits().plan_max_search_window_minutes_,\n      \"maximum searchWindow size exceeded\");\n\n  auto const max_transfers =\n      query.maxTransfers_.has_value() &&\n              *query.maxTransfers_ <= n::routing::kMaxTransfers\n          ? (*query.maxTransfers_ - (api_version < 3 ? 1 : 0))\n          : n::routing::kMaxTransfers;\n  auto const osr_params = get_osr_parameters(query);\n  auto const detailed_transfers =\n      query.detailedTransfers_.value_or(query.detailedLegs_);\n\n  auto const [start_time, t] = get_start_time(query, tt_);\n\n  UTL_START_TIMING(direct);\n  auto [direct, fastest_direct] =\n      t.has_value() && !direct_modes.empty() && w_ && l_\n          ? route_direct(\n                e, gbfs_rd, lang, from_p, to_p, direct_modes,\n                query.directRentalFormFactors_,\n                query.directRentalPropulsionTypes_,\n                query.directRentalProviders_, query.directRentalProviderGroups_,\n                query.ignoreDirectRentalReturnConstraints_, *t, query.arriveBy_,\n                osr_params, query.pedestrianProfile_, query.elevationCosts_,\n                std::min(std::chrono::seconds{query.maxDirectTime_},\n                         std::chrono::seconds{\n                             config_.get_limits()\n                                 .street_routing_max_direct_seconds_}),\n                query.maxMatchingDistance_, query.fastestDirectFactor_,\n                query.detailedLegs_, api_version)\n          : std::pair{std::vector<api::Itinerary>{}, kInfinityDuration};\n  UTL_STOP_TIMING(direct);\n\n  if (!query.transitModes_.empty() && fastest_direct > 5min &&\n      max_transfers >= 0) {\n    utl::verify(tt_ != nullptr && tags_ != nullptr && loc_tree_ != nullptr,\n                \"mode=TRANSIT requires timetable to be loaded\");\n\n    auto const max_results = config_.get_limits().plan_max_results_;\n    utl::verify<net::too_many_exception>(\n        query.numItineraries_ <= max_results,\n        \"maximum number of minimum itineraries is {}\", max_results);\n    auto const max_timeout =\n        std::chrono::seconds{config_.get_limits().routing_max_timeout_seconds_};\n    utl::verify<net::too_many_exception>(\n        !query.timeout_.has_value() ||\n            std::chrono::seconds{*query.timeout_} <= max_timeout,\n        \"maximum allowed timeout is {}\", max_timeout);\n\n    auto const with_odm_pre_transit =\n        utl::find(pre_transit_modes, api::ModeEnum::ODM) !=\n        end(pre_transit_modes);\n    auto const with_odm_post_transit =\n        utl::find(post_transit_modes, api::ModeEnum::ODM) !=\n        end(post_transit_modes);\n    auto const with_odm_direct =\n        utl::find(direct_modes, api::ModeEnum::ODM) != end(direct_modes);\n    auto const with_ride_sharing_pre_transit =\n        utl::find(pre_transit_modes, api::ModeEnum::RIDE_SHARING) !=\n        end(pre_transit_modes);\n    auto const with_ride_sharing_post_transit =\n        utl::find(post_transit_modes, api::ModeEnum::RIDE_SHARING) !=\n        end(post_transit_modes);\n    auto const with_ride_sharing_direct =\n        utl::find(direct_modes, api::ModeEnum::RIDE_SHARING) !=\n        end(direct_modes);\n\n    if (with_odm_pre_transit || with_odm_post_transit || with_odm_direct ||\n        with_ride_sharing_pre_transit || with_ride_sharing_post_transit ||\n        with_ride_sharing_direct) {\n      utl::verify(config_.has_prima(), \"PRIMA not configured\");\n      return odm::meta_router{*this,\n                              query,\n                              pre_transit_modes,\n                              post_transit_modes,\n                              direct_modes,\n                              from,\n                              to,\n                              from_p,\n                              to_p,\n                              start_time,\n                              direct,\n                              fastest_direct,\n                              with_odm_pre_transit,\n                              with_odm_post_transit,\n                              with_odm_direct,\n                              with_ride_sharing_pre_transit,\n                              with_ride_sharing_post_transit,\n                              with_ride_sharing_direct,\n                              api_version}\n          .run();\n    }\n\n    auto const pre_transit_time = std::min(\n        std::chrono::seconds{query.maxPreTransitTime_},\n        std::chrono::seconds{\n            config_.get_limits().street_routing_max_prepost_transit_seconds_});\n    auto const post_transit_time = std::min(\n        std::chrono::seconds{query.maxPostTransitTime_},\n        std::chrono::seconds{\n            config_.get_limits().street_routing_max_prepost_transit_seconds_});\n\n    UTL_START_TIMING(query_preparation);\n    auto prepare_stats = std::map<std::string, std::uint64_t>{};\n\n    auto const use_radius_start = query.radius_.has_value() &&\n                                  std::holds_alternative<osr::location>(start);\n    auto const use_radius_dest = query.radius_.has_value() &&\n                                 std::holds_alternative<osr::location>(dest);\n\n    auto q = n::routing::query{\n        .start_time_ = start_time.start_time_,\n        .start_match_mode_ = use_radius_start\n                                 ? n::routing::location_match_mode::kIntermodal\n                                 : get_match_mode(*this, start),\n        .dest_match_mode_ = use_radius_dest\n                                ? n::routing::location_match_mode::kIntermodal\n                                : get_match_mode(*this, dest),\n        .use_start_footpaths_ =\n            !use_radius_start && !is_intermodal(*this, start),\n        .start_ =\n            use_radius_start\n                ? radius_offsets(*loc_tree_,\n                                 std::get<osr::location>(start).pos_,\n                                 *query.radius_)\n                : get_offsets(\n                      rtt, start,\n                      query.arriveBy_ ? osr::direction::kBackward\n                                      : osr::direction::kForward,\n                      start_modes, start_form_factors, start_propulsion_types,\n                      start_rental_providers, start_rental_provider_groups,\n                      start_ignore_return_constraints, osr_params,\n                      query.pedestrianProfile_, query.elevationCosts_,\n                      query.arriveBy_ ? post_transit_time : pre_transit_time,\n                      query.maxMatchingDistance_, gbfs_rd, prepare_stats),\n        .destination_ =\n            use_radius_dest\n                ? radius_offsets(*loc_tree_, std::get<osr::location>(dest).pos_,\n                                 *query.radius_)\n                : get_offsets(\n                      rtt, dest,\n                      query.arriveBy_ ? osr::direction::kForward\n                                      : osr::direction::kBackward,\n                      dest_modes, dest_form_factors, dest_propulsion_types,\n                      dest_rental_providers, dest_rental_provider_groups,\n                      dest_ignore_return_constraints, osr_params,\n                      query.pedestrianProfile_, query.elevationCosts_,\n                      query.arriveBy_ ? pre_transit_time : post_transit_time,\n                      query.maxMatchingDistance_, gbfs_rd, prepare_stats),\n        .td_start_ = get_td_offsets(\n            rtt, e, start,\n            query.arriveBy_ ? osr::direction::kBackward\n                            : osr::direction::kForward,\n            start_modes, osr_params, query.pedestrianProfile_,\n            query.elevationCosts_, query.maxMatchingDistance_,\n            query.arriveBy_ ? post_transit_time : pre_transit_time,\n            start_time.start_time_, prepare_stats),\n        .td_dest_ = get_td_offsets(\n            rtt, e, dest,\n            query.arriveBy_ ? osr::direction::kForward\n                            : osr::direction::kBackward,\n            dest_modes, osr_params, query.pedestrianProfile_,\n            query.elevationCosts_, query.maxMatchingDistance_,\n            query.arriveBy_ ? pre_transit_time : post_transit_time,\n            start_time.start_time_, prepare_stats),\n        .max_transfers_ = static_cast<std::uint8_t>(max_transfers),\n        .max_travel_time_ = query.maxTravelTime_\n                                .and_then([](std::int64_t const dur) {\n                                  return std::optional{n::duration_t{dur}};\n                                })\n                                .value_or(kInfinityDuration),\n        .min_connection_count_ = static_cast<unsigned>(query.numItineraries_),\n        .extend_interval_earlier_ = start_time.extend_interval_earlier_,\n        .extend_interval_later_ = start_time.extend_interval_later_,\n        .prf_idx_ = static_cast<n::profile_idx_t>(\n            query.useRoutedTransfers_\n                ? query.requireCarTransport_ ? n::kCarProfile\n                  : query.pedestrianProfile_ ==\n                          api::PedestrianProfileEnum::WHEELCHAIR\n                      ? n::kWheelchairProfile\n                      : n::kFootProfile\n                : 0U),\n        .allowed_claszes_ = to_clasz_mask(query.transitModes_),\n        .require_bike_transport_ = query.requireBikeTransport_,\n        .require_car_transport_ = query.requireCarTransport_,\n        .transfer_time_settings_ =\n            n::routing::transfer_time_settings{\n                .default_ = (query.minTransferTime_ == 0 &&\n                             query.additionalTransferTime_ == 0 &&\n                             query.transferTimeFactor_ == 1.0),\n                .min_transfer_time_ = n::duration_t{query.minTransferTime_},\n                .additional_time_ =\n                    n::duration_t{query.additionalTransferTime_},\n                .factor_ = static_cast<float>(query.transferTimeFactor_)},\n        .via_stops_ = get_via_stops(*tt_, *tags_, query.via_,\n                                    query.viaMinimumStay_, query.arriveBy_),\n        .fastest_direct_ = fastest_direct == kInfinityDuration\n                               ? std::nullopt\n                               : std::optional{fastest_direct},\n        .fastest_direct_factor_ = query.fastestDirectFactor_,\n        .slow_direct_ = query.slowDirect_,\n        .fastest_slow_direct_factor_ = query.fastestSlowDirectFactor_};\n    remove_slower_than_fastest_direct(q);\n    UTL_STOP_TIMING(query_preparation);\n\n    if (tt_->locations_.footpaths_out_.at(q.prf_idx_).empty()) {\n      q.prf_idx_ = 0U;\n    }\n\n    auto const query_stats =\n        stats_map_t{{\"direct\", UTL_TIMING_MS(direct)},\n                    {\"prepare\", UTL_TIMING_MS(query_preparation)},\n                    {\"n_start_offsets\", q.start_.size()},\n                    {\"n_dest_offsets\", q.destination_.size()},\n                    {\"n_td_start_offsets\", q.td_start_.size()},\n                    {\"n_td_dest_offsets\", q.td_dest_.size()}};\n\n    auto r = n::routing::routing_result{};\n    auto algorithm = query.algorithm_;\n    auto search_state = n::routing::search_state{};\n    while (true) {\n      if (algorithm == api::algorithmEnum::PONG && query.timetableView_ &&\n          // arriveBy |  extend_later | PONG applicable\n          // ---------+---------------+---------------------\n          // FALSE    |  FALSE        | FALSE    => rRAPTOR\n          // FALSE    |  TRUE         | TRUE     => PONG\n          // TRUE     |  FALSE        | TRUE     => PONG\n          // TRUE     |  TRUE         | FALSE    => rRAPTOR\n          query.arriveBy_ != start_time.extend_interval_later_) {\n        try {\n          auto raptor_state = n::routing::raptor_state{};\n          r = n::routing::pong_search(\n              *tt_, rtt, search_state, raptor_state, q,\n              query.arriveBy_ ? n::direction::kBackward\n                              : n::direction::kForward,\n              query.timeout_.has_value() ? std::chrono::seconds{*query.timeout_}\n                                         : max_timeout);\n        } catch (std::exception const& e) {\n          std::cout << \"PONG EXCEPTION: \" << e.what() << \"\\n\";\n          algorithm = api::algorithmEnum::RAPTOR;\n          continue;\n        }\n      } else if (algorithm == api::algorithmEnum::RAPTOR || tbd_ == nullptr ||\n                 (rtt != nullptr && rtt->n_rt_transports() != 0U) ||\n                 query.arriveBy_ || q.prf_idx_ != tbd_->prf_idx_ ||\n                 q.allowed_claszes_ != n::routing::all_clasz_allowed() ||\n                 !q.td_start_.empty() || !q.td_dest_.empty() ||\n                 !q.transfer_time_settings_.default_ || !q.via_stops_.empty() ||\n                 q.require_bike_transport_ || q.require_car_transport_) {\n        auto raptor_state = n::routing::raptor_state{};\n        r = n::routing::raptor_search(\n            *tt_, rtt, search_state, raptor_state, q,\n            query.arriveBy_ ? n::direction::kBackward : n::direction::kForward,\n            query.timeout_.has_value() ? std::chrono::seconds{*query.timeout_}\n                                       : max_timeout);\n      } else {\n        auto tb_state = n::routing::tb::query_state{*tt_, *tbd_};\n        r = n::routing::tb::tb_search(*tt_, search_state, tb_state, q);\n      }\n      break;\n    }\n\n    metrics_->routing_journeys_found_.Increment(\n        static_cast<double>(r.journeys_->size()));\n    metrics_->routing_execution_duration_seconds_total_.Observe(\n        static_cast<double>(r.search_stats_.execute_time_.count()) / 1000.0);\n\n    if (!r.journeys_->empty()) {\n      metrics_->routing_journey_duration_seconds_.Observe(static_cast<double>(\n          to_seconds(r.journeys_->begin()->arrival_time() -\n                     r.journeys_->begin()->departure_time())));\n    }\n\n    auto journeys = r.journeys_->els_;\n    auto search_interval = r.interval_;\n    if (query.maxItineraries_.has_value()) {\n      search_interval = shrink(start_time.extend_interval_earlier_,\n                               static_cast<std::size_t>(*query.maxItineraries_),\n                               r.interval_, journeys);\n    }\n\n    direct_filter(direct, journeys);\n\n    return {\n        .debugOutput_ =\n            join(std::move(prepare_stats), std::move(query_stats),\n                 r.search_stats_.to_map(), std::move(r.algo_stats_)),\n        .from_ = from_p,\n        .to_ = to_p,\n        .direct_ = std::move(direct),\n        .itineraries_ = utl::to_vec(\n            journeys,\n            [&, cache = street_routing_cache_t{}](auto&& j) mutable {\n              return journey_to_response(\n                  w_, l_, pl_, *tt_, *tags_, fa_, e, rtt, matches_, elevations_,\n                  shapes_, gbfs_rd, ae_, tz_, j, start, dest, cache,\n                  blocked.get(),\n                  query.requireCarTransport_ && query.useRoutedTransfers_,\n                  osr_params, query.pedestrianProfile_, query.elevationCosts_,\n                  query.joinInterlinedLegs_, detailed_transfers,\n                  query.detailedLegs_, query.withFares_,\n                  query.withScheduledSkippedStops_,\n                  config_.timetable_.value().max_matching_distance_,\n                  query.maxMatchingDistance_, api_version,\n                  query.ignorePreTransitRentalReturnConstraints_,\n                  query.ignorePostTransitRentalReturnConstraints_,\n                  query.language_);\n            }),\n        .previousPageCursor_ =\n            fmt::format(\"EARLIER|{}\", to_seconds(search_interval.from_)),\n        .nextPageCursor_ =\n            fmt::format(\"LATER|{}\", to_seconds(search_interval.to_)),\n    };\n  }\n\n  return {\n      .from_ = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, from),\n      .to_ = to_place(tt_, tags_, w_, pl_, matches_, ae_, tz_, lang, to),\n      .direct_ = std::move(direct),\n      .itineraries_ = {}};\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/stop_times.cc",
    "content": "#include \"motis/endpoints/stop_times.h\"\n\n#include <algorithm>\n#include <memory>\n\n#include \"utl/concat.h\"\n#include \"utl/enumerate.h\"\n#include \"utl/erase_duplicates.h\"\n#include \"utl/verify.h\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/not_found_exception.h\"\n#include \"net/too_many_exception.h\"\n\n#include \"nigiri/routing/clasz_mask.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/rt/run.h\"\n#include \"nigiri/timetable.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/data.h\"\n#include \"motis/journey_to_response.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/place.h\"\n#include \"motis/server.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/clasz_to_mode.h\"\n#include \"motis/timetable/modes_to_clasz_mask.h\"\n#include \"motis/timetable/time_conv.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\nstruct ev_iterator {\n  ev_iterator() = default;\n  ev_iterator(ev_iterator const&) = delete;\n  ev_iterator(ev_iterator&&) = delete;\n  ev_iterator& operator=(ev_iterator const&) = delete;\n  ev_iterator& operator=(ev_iterator&&) = delete;\n  virtual ~ev_iterator() = default;\n  virtual bool finished() const = 0;\n  virtual n::unixtime_t time() const = 0;\n  virtual n::rt::run get() const = 0;\n  virtual void increment() = 0;\n};\n\nstruct static_ev_iterator : public ev_iterator {\n  static_ev_iterator(n::timetable const& tt,\n                     n::rt_timetable const* rtt,\n                     n::route_idx_t const r,\n                     n::stop_idx_t const stop_idx,\n                     n::unixtime_t const start,\n                     n::event_type const ev_type,\n                     n::direction const dir)\n      : tt_{tt},\n        rtt_{rtt},\n        day_{to_idx(tt_.day_idx_mam(start).first)},\n        end_day_{dir == n::direction::kForward\n                     ? to_idx(tt.day_idx(tt_.date_range_.to_))\n                     : to_idx(tt.day_idx(tt_.date_range_.from_) - 1U)},\n        size_{tt_.route_transport_ranges_[r].size()},\n        i_{0},\n        r_{r},\n        stop_idx_{stop_idx},\n        ev_type_{ev_type},\n        dir_{dir} {\n    seek_next(start);\n  }\n\n  ~static_ev_iterator() override = default;\n\n  static_ev_iterator(static_ev_iterator const&) = delete;\n  static_ev_iterator(static_ev_iterator&&) = delete;\n  static_ev_iterator& operator=(static_ev_iterator const&) = delete;\n  static_ev_iterator& operator=(static_ev_iterator&&) = delete;\n\n  void seek_next(std::optional<n::unixtime_t> const start = std::nullopt) {\n    while (!finished()) {\n      for (; i_ < size_; ++i_) {\n        if (start.has_value() &&\n            (dir_ == n::direction::kForward ? time() < *start\n                                            : time() > *start)) {\n          continue;\n        }\n        if (is_active()) {\n          return;\n        }\n      }\n      dir_ == n::direction::kForward ? ++day_ : --day_;\n      i_ = 0;\n    }\n  }\n\n  bool finished() const override { return day_ == end_day_; }\n\n  n::unixtime_t time() const override {\n    return tt_.event_time(t(), stop_idx_, ev_type_);\n  }\n\n  n::rt::run get() const override {\n    assert(is_active());\n    return n::rt::run{\n        .t_ = t(),\n        .stop_range_ = {stop_idx_, static_cast<n::stop_idx_t>(stop_idx_ + 1U)}};\n  }\n\n  void increment() override {\n    ++i_;\n    seek_next();\n  }\n\nprivate:\n  bool is_active() const {\n    auto const x = t();\n    auto const in_static =\n        tt_.bitfields_[tt_.transport_traffic_days_[x.t_idx_]].test(\n            to_idx(x.day_));\n    return rtt_ == nullptr\n               ? in_static\n               : in_static &&\n                     rtt_->resolve_rt(x) ==  // only when no RT/cancelled\n                         n::rt_transport_idx_t::invalid();\n  }\n\n  n::transport t() const {\n    auto const idx = dir_ == n::direction::kForward ? i_ : size_ - i_ - 1;\n    auto const t = tt_.route_transport_ranges_[r_][idx];\n    auto const day_offset = tt_.event_mam(r_, t, stop_idx_, ev_type_).days();\n    return n::transport{tt_.route_transport_ranges_[r_][idx],\n                        n::day_idx_t{to_idx(day_) - day_offset}};\n  }\n\n  n::timetable const& tt_;\n  n::rt_timetable const* rtt_;\n  n::day_idx_t day_, end_day_;\n  std::uint32_t size_;\n  std::uint32_t i_;\n  n::route_idx_t r_;\n  n::stop_idx_t stop_idx_;\n  n::event_type ev_type_;\n  n::direction dir_;\n};\n\nstruct rt_ev_iterator : public ev_iterator {\n  rt_ev_iterator(n::rt_timetable const& rtt,\n                 n::rt_transport_idx_t const rt_t,\n                 n::stop_idx_t const stop_idx,\n                 n::unixtime_t const start,\n                 n::event_type const ev_type,\n                 n::direction const dir,\n                 n::routing::clasz_mask_t const allowed_clasz)\n      : rtt_{rtt},\n        stop_idx_{stop_idx},\n        rt_t_{rt_t},\n        ev_type_{ev_type},\n        finished_{\n            !n::routing::is_allowed(\n                allowed_clasz, rtt.rt_transport_section_clasz_[rt_t].at(0)) ||\n            (dir == n::direction::kForward ? time() < start : time() > start)} {\n    assert((ev_type == n::event_type::kDep &&\n            stop_idx_ < rtt_.rt_transport_location_seq_[rt_t].size() - 1U) ||\n           (ev_type == n::event_type::kArr && stop_idx_ > 0U));\n  }\n\n  ~rt_ev_iterator() override = default;\n\n  rt_ev_iterator(rt_ev_iterator const&) = delete;\n  rt_ev_iterator(rt_ev_iterator&&) = delete;\n  rt_ev_iterator& operator=(rt_ev_iterator const&) = delete;\n  rt_ev_iterator& operator=(rt_ev_iterator&&) = delete;\n\n  bool finished() const override { return finished_; }\n\n  n::unixtime_t time() const override {\n    return rtt_.unix_event_time(rt_t_, stop_idx_, ev_type_);\n  }\n\n  n::rt::run get() const override {\n    return n::rt::run{\n        .stop_range_ = {stop_idx_, static_cast<n::stop_idx_t>(stop_idx_ + 1U)},\n        .rt_ = rt_t_};\n  }\n\n  void increment() override { finished_ = true; }\n\n  n::rt_timetable const& rtt_;\n  n::stop_idx_t stop_idx_;\n  n::rt_transport_idx_t rt_t_;\n  n::event_type ev_type_;\n  bool finished_{false};\n};\n\nstd::vector<n::rt::run> get_events(\n    std::vector<n::location_idx_t> const& locations,\n    n::timetable const& tt,\n    n::rt_timetable const* rtt,\n    n::unixtime_t const time,\n    n::event_type const ev_type,\n    n::direction const dir,\n    std::size_t const min_count,\n    std::size_t const max_count,\n    n::routing::clasz_mask_t const allowed_clasz,\n    bool const with_scheduled_skipped_stops,\n    std::optional<n::duration_t> const max_time_diff) {\n  auto iterators = std::vector<std::unique_ptr<ev_iterator>>{};\n\n  if (rtt != nullptr) {\n    for (auto const x : locations) {\n      for (auto const rt_t : rtt->location_rt_transports_[x]) {\n        auto const location_seq = rtt->rt_transport_location_seq_[rt_t];\n        for (auto const [stop_idx, s] : utl::enumerate(location_seq)) {\n          if (n::stop{s}.location_idx() == x &&\n              ((ev_type == n::event_type::kDep &&\n                stop_idx != location_seq.size() - 1U) ||\n               (ev_type == n::event_type::kArr && stop_idx != 0U))) {\n            if (!with_scheduled_skipped_stops) {\n              auto const fr = n::rt::frun{\n                  tt, rtt,\n                  n::rt::run{\n                      .stop_range_ = {static_cast<n::stop_idx_t>(stop_idx),\n                                      static_cast<n::stop_idx_t>(stop_idx +\n                                                                 1U)},\n                      .rt_ = rt_t}};\n              auto const frs = fr[0];\n              if ((ev_type == n::event_type::kDep &&\n                   !frs.get_scheduled_stop().in_allowed() &&\n                   !frs.in_allowed()) ||\n                  (ev_type == n::event_type::kArr &&\n                   !frs.get_scheduled_stop().out_allowed() &&\n                   !frs.out_allowed())) {\n                continue;\n              }\n            }\n            iterators.emplace_back(std::make_unique<rt_ev_iterator>(\n                *rtt, rt_t, static_cast<n::stop_idx_t>(stop_idx), time, ev_type,\n                dir, allowed_clasz));\n          }\n        }\n      }\n    }\n  }\n\n  auto seen = n::hash_set<std::pair<n::route_idx_t, n::stop_idx_t>>{};\n  for (auto const x : locations) {\n    for (auto const r : tt.location_routes_[x]) {\n      if (!n::routing::is_allowed(allowed_clasz, tt.route_clasz_[r])) {\n        continue;\n      }\n      auto const location_seq = tt.route_location_seq_[r];\n      for (auto const [stop_idx, s] : utl::enumerate(location_seq)) {\n        if (n::stop{s}.location_idx() == x &&\n            ((ev_type == n::event_type::kDep &&\n              stop_idx != location_seq.size() - 1U &&\n              (with_scheduled_skipped_stops || n::stop{s}.in_allowed())) ||\n             (ev_type == n::event_type::kArr && stop_idx != 0U &&\n              (with_scheduled_skipped_stops || n::stop{s}.out_allowed()))) &&\n            seen.emplace(r, static_cast<n::stop_idx_t>(stop_idx)).second) {\n          iterators.emplace_back(std::make_unique<static_ev_iterator>(\n              tt, rtt, r, static_cast<n::stop_idx_t>(stop_idx), time, ev_type,\n              dir));\n        }\n      }\n    }\n  }\n\n  auto const all_finished = [&]() {\n    return utl::all_of(iterators,\n                       [](auto const& it) { return it->finished(); });\n  };\n\n  auto const fwd = dir == n::direction::kForward;\n  auto evs = std::vector<n::rt::run>{};\n  auto last_time = n::unixtime_t{};\n  while (!all_finished()) {\n    auto const it = std::min_element(\n        begin(iterators), end(iterators), [&](auto const& a, auto const& b) {\n          if (a->finished() || b->finished()) {\n            return a->finished() < b->finished();\n          }\n          return fwd ? a->time() < b->time() : a->time() > b->time();\n        });\n    assert(!(*it)->finished());\n    auto const current_time = (*it)->time();\n    if ((!max_time_diff.has_value() ||\n         std::chrono::abs(current_time - time) > *max_time_diff) &&\n        (evs.size() >= min_count && current_time != last_time)) {\n      break;\n    }\n    evs.emplace_back((*it)->get());\n    utl::verify<net::too_many_exception>(\n        evs.size() <= max_count,\n        \"requesting for more than {} datapoints is not allowed\", max_count);\n    last_time = current_time;\n    (*it)->increment();\n  }\n  return evs;\n}\n\nstd::vector<api::Place> other_stops_impl(n::rt::frun fr,\n                                         n::event_type ev_type,\n                                         n::timetable const* tt,\n                                         tag_lookup const& tags,\n                                         osr::ways const* w,\n                                         osr::platforms const* pl,\n                                         platform_matches_t const* matches,\n                                         adr_ext const* ae,\n                                         tz_map_t const* tz,\n                                         n::lang_t const& lang) {\n  auto const convert_stop = [&](n::rt::run_stop const& stop) {\n    auto result = to_place(tt, &tags, w, pl, matches, ae, tz, lang, stop);\n    if (ev_type == n::event_type::kDep ||\n        stop.fr_->stop_range_.from_ != stop.stop_idx_) {\n      result.arrival_ = stop.time(n::event_type::kArr);\n      result.scheduledArrival_ = stop.scheduled_time(n::event_type::kArr);\n    }\n    if (ev_type == n::event_type::kArr ||\n        stop.fr_->stop_range_.to_ - 1 != stop.stop_idx_) {\n      result.departure_ = stop.time(n::event_type::kDep);\n      result.scheduledDeparture_ = stop.scheduled_time(n::event_type::kDep);\n    }\n    return result;\n  };\n\n  auto const orig_location = fr[fr.first_valid()].get_location_idx();\n  if (ev_type == nigiri::event_type::kDep) {\n    ++fr.stop_range_.from_;\n    fr.stop_range_.to_ = fr.size();\n    // Return next stops until one stop before the loop closes\n    auto const it =\n        utl::find_if(fr, [orig_location](n::rt::run_stop const& stop) {\n          return orig_location == stop.get_location_idx();\n        });\n    auto result = utl::to_vec(fr.begin(), it, convert_stop);\n    utl::verify<net::bad_request_exception>(!result.empty(),\n                                            \"Departure is last stop in trip\");\n    return result;\n  } else {\n    fr.stop_range_.from_ = 0;\n    --fr.stop_range_.to_;\n    // Return previous stops beginning one stop before the loop closes\n    auto const it = std::find_if(\n        fr.rbegin(), fr.rend(), [orig_location](n::rt::run_stop const& stop) {\n          return orig_location == stop.get_location_idx();\n        });\n    auto result = utl::to_vec(it.base(), fr.end(), convert_stop);\n    utl::verify<net::bad_request_exception>(!result.empty(),\n                                            \"Arrival is first stop in trip\");\n    return result;\n  }\n}\n\napi::stoptimes_response stop_times::operator()(\n    boost::urls::url_view const& url) const {\n  auto const query = api::stoptimes_params{url.params()};\n  auto const& lang = query.language_;\n  auto const api_version = get_api_version(url);\n\n  auto const max_results = config_.get_limits().stoptimes_max_results_;\n  utl::verify<net::bad_request_exception>(\n      query.n_.has_value() || query.window_.has_value(),\n      \"neither 'n' nor 'window' is provided\");\n  if (query.n_.has_value()) {\n    utl::verify<net::too_many_exception>(*query.n_ <= max_results,\n                                         \"n={} > {} not allowed\", *query.n_,\n                                         max_results);\n  }\n  utl::verify<net::bad_request_exception>(\n      query.stopId_.has_value() ||\n          (query.center_.has_value() && query.radius_.has_value()),\n      \"no stop and no center with radius (at least one is required)\");\n\n  auto const query_stop = query.stopId_.and_then(\n      [&](std::string const& x) { return tags_.find_location(tt_, x); });\n\n  auto const query_center = query.center_.and_then(\n      [&](std::string const& x) { return parse_location(x); });\n\n  auto const center =\n      query_stop\n          .transform([&](n::location_idx_t const l) {\n            return tt_.locations_.coordinates_[l];\n          })\n          .or_else([&]() {\n            return query_center.transform(\n                [](osr::location const& loc) { return loc.pos_; });\n          });\n\n  utl::verify<net::not_found_exception>(\n      query_stop.has_value() ||\n          (query_center.has_value() && query.radius_.has_value()),\n      \"no radius: stop_found={}, center_parsed={}\", query_stop.has_value(),\n      query_center.has_value());\n\n  utl::verify<net::bad_request_exception>(\n      center.has_value(), \"no coordinates: stop_found={}, center_parsed={}\",\n      query_stop.has_value(), query_center.has_value());\n\n  auto const allowed_clasz = to_clasz_mask(query.mode_);\n  auto const [dir, time] = parse_cursor(query.pageCursor_.value_or(fmt::format(\n      \"{}|{}\",\n      query.direction_\n          .transform([](auto&& x) {\n            return x == api::directionEnum::EARLIER ? \"EARLIER\" : \"LATER\";\n          })\n          .value_or(query.arriveBy_ ? \"EARLIER\" : \"LATER\"),\n      std::chrono::duration_cast<std::chrono::seconds>(\n          query.time_.value_or(openapi::now())->time_since_epoch())\n          .count())));\n\n  auto locations = std::vector<n::location_idx_t>{};\n  auto const add = [&](n::location_idx_t const l) {\n    if (query.exactRadius_) {\n      locations.emplace_back(l);\n      return;\n    }\n\n    auto const l_name = tt_.get_default_translation(tt_.locations_.names_[l]);\n    utl::concat(locations, tt_.locations_.children_[l]);\n    for (auto const& c : tt_.locations_.children_[l]) {\n      utl::concat(locations, tt_.locations_.children_[c]);\n    }\n\n    for (auto const eq : tt_.locations_.equivalences_[l]) {\n      if (tt_.get_default_translation(tt_.locations_.names_[eq]) == l_name) {\n        locations.emplace_back(eq);\n        utl::concat(locations, tt_.locations_.children_[eq]);\n        for (auto const& c : tt_.locations_.children_[eq]) {\n          utl::concat(locations, tt_.locations_.children_[c]);\n        }\n      }\n    }\n  };\n\n  if (query_stop.has_value()) {\n    locations.emplace_back(tt_.locations_.get_root_idx(*query_stop));\n    if (query.radius_) {\n      loc_rtree_.in_radius(*center, static_cast<double>(*query.radius_), add);\n    } else {\n      add(*query_stop);\n    }\n  } else {\n    loc_rtree_.in_radius(*center, static_cast<double>(query.radius_.value()),\n                         add);\n  }\n  utl::erase_duplicates(locations);\n\n  auto const rt = std::atomic_load(&rt_);\n  auto const rtt = rt->rtt_.get();\n  auto const ev_type =\n      query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep;\n  auto const window = query.window_.transform([](auto const w) {\n    return std::chrono::duration_cast<n::duration_t>(std::chrono::seconds{w});\n  });\n  auto events = get_events(locations, tt_, rtt, time, ev_type, dir,\n                           static_cast<std::size_t>(query.n_.value_or(0)),\n                           static_cast<std::size_t>(max_results), allowed_clasz,\n                           query.withScheduledSkippedStops_, window);\n\n  auto const to_tuple = [&](n::rt::run const& x) {\n    auto const fr_a = n::rt::frun{tt_, rtt, x};\n    return std::tuple{fr_a[0].time(ev_type), fr_a.is_scheduled()\n                                                 ? fr_a[0].get_trip_idx(ev_type)\n                                                 : n::trip_idx_t::invalid()};\n  };\n  utl::sort(events, [&](n::rt::run const& a, n::rt::run const& b) {\n    return to_tuple(a) < to_tuple(b);\n  });\n  events.erase(std::unique(begin(events), end(events),\n                           [&](n::rt::run const& a, n::rt::run const& b) {\n                             return to_tuple(a) == to_tuple(b);\n                           }),\n               end(events));\n  return {\n      .stopTimes_ = utl::to_vec(\n          events,\n          [&](n::rt::run const r) -> api::StopTime {\n            auto const fr = n::rt::frun{tt_, rtt, r};\n            auto const s = fr[0];\n            auto const& agency = s.get_provider(ev_type);\n            auto const run_cancelled = fr.is_cancelled();\n            auto place = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_,\n                                  query.language_, s);\n            if (query.withAlerts_) {\n              place.alerts_ =\n                  get_alerts(fr,\n                             std::pair{s, fr.stop_range_.from_ != 0U\n                                              ? n::event_type::kArr\n                                              : n::event_type::kDep},\n                             true, query.language_);\n            }\n            if (fr.stop_range_.from_ != 0U) {\n              place.arrival_ = {s.time(n::event_type::kArr)};\n              place.scheduledArrival_ = {s.scheduled_time(n::event_type::kArr)};\n            }\n            if (fr.stop_range_.from_ != fr.size() - 1U) {\n              place.departure_ = {s.time(n::event_type::kDep)};\n              place.scheduledDeparture_ = {\n                  s.scheduled_time(n::event_type::kDep)};\n            }\n            auto const in_out_allowed =\n                !run_cancelled &&\n                (ev_type == n::event_type::kArr ? s.out_allowed()\n                                                : s.in_allowed());\n            auto const stop_cancelled =\n                run_cancelled ||\n                (ev_type == n::event_type::kArr\n                     ? !s.out_allowed() && s.get_scheduled_stop().out_allowed()\n                     : !s.in_allowed() && s.get_scheduled_stop().in_allowed());\n\n            auto const trip_id = tags_.id(tt_, s, ev_type);\n\n            auto const other_stops = [&](n::event_type desired_event)\n                -> std::optional<std::vector<api::Place>> {\n              if (desired_event != ev_type ||\n                  !query.fetchStops_.value_or(false)) {\n                return std::nullopt;\n              }\n              return other_stops_impl(fr, ev_type, &tt_, tags_, w_, pl_,\n                                      matches_, ae_, tz_, query.language_);\n            };\n\n            return {\n                .place_ = std::move(place),\n                .mode_ = to_mode(s.get_clasz(ev_type), api_version),\n                .realTime_ = r.is_rt(),\n                .headsign_ = std::string{s.direction(lang, ev_type)},\n                .tripFrom_ = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_,\n                                      lang, s.get_first_trip_stop(ev_type)),\n                .tripTo_ = to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_,\n                                    lang, s.get_last_trip_stop(ev_type)),\n                .agencyId_ =\n                    std::string{tt_.strings_.try_get(agency.id_).value_or(\"?\")},\n                .agencyName_ = std::string{tt_.translate(lang, agency.name_)},\n                .agencyUrl_ = std::string{tt_.translate(lang, agency.url_)},\n                .routeId_ = tags_.route_id(s, ev_type),\n                .routeUrl_ = std::string{s.route_url(ev_type, lang)},\n                .directionId_ = s.get_direction_id(ev_type) == 0 ? \"0\" : \"1\",\n                .routeColor_ = to_str(s.get_route_color(ev_type).color_),\n                .routeTextColor_ =\n                    to_str(s.get_route_color(ev_type).text_color_),\n                .tripId_ = trip_id,\n                .routeType_ =\n                    s.route_type(ev_type).and_then([](n::route_type_t const x) {\n                      return std::optional{to_idx(x)};\n                    }),\n                .routeShortName_ =\n                    std::string{api_version < 4\n                                    ? s.display_name(ev_type, lang)\n                                    : s.route_short_name(ev_type, lang)},\n                .routeLongName_ = std::string{s.route_long_name(ev_type, lang)},\n                .tripShortName_ = std::string{s.trip_short_name(ev_type, lang)},\n                .displayName_ = std::string{s.display_name(ev_type, lang)},\n                .previousStops_ = other_stops(nigiri::event_type::kArr),\n                .nextStops_ = other_stops(nigiri::event_type::kDep),\n                .pickupDropoffType_ =\n                    in_out_allowed ? api::PickupDropoffTypeEnum::NORMAL\n                                   : api::PickupDropoffTypeEnum::NOT_ALLOWED,\n                .cancelled_ = stop_cancelled,\n                .tripCancelled_ = run_cancelled,\n                .source_ = fmt::format(\"{}\", fmt::streamed(fr.dbg()))};\n          }),\n      .place_ =\n          query_stop\n              .transform([&](n::location_idx_t const l) {\n                return to_place(&tt_, &tags_, w_, pl_, matches_, ae_, tz_, lang,\n                                tt_location{l});\n              })\n              .or_else([&]() {\n                return query_center.transform([](osr::location const& loc) {\n                  return to_place(loc, \"center\", std::nullopt);\n                });\n              })\n              .value(),\n      .previousPageCursor_ =\n          events.empty()\n              ? \"\"\n              : fmt::format(\n                    \"EARLIER|{}\",\n                    to_seconds(\n                        n::rt::frun{tt_, rtt, events.front()}[0].time(ev_type) -\n                        std::chrono::minutes{1})),\n      .nextPageCursor_ =\n          events.empty()\n              ? \"\"\n              : fmt::format(\n                    \"LATER|{}\",\n                    to_seconds(\n                        n::rt::frun{tt_, rtt, events.back()}[0].time(ev_type) +\n                        std::chrono::minutes{1}))};\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/tiles.cc",
    "content": "#include \"motis/endpoints/tiles.h\"\n\n#include <string>\n\n#include \"net/web_server/url_decode.h\"\n\n#include \"tiles/get_tile.h\"\n#include \"tiles/parse_tile_url.h\"\n#include \"tiles/perf_counter.h\"\n\n#include \"motis/tiles_data.h\"\n\n#include \"pbf_sdf_fonts_res.h\"\n\nusing namespace std::string_view_literals;\n\nnamespace motis::ep {\n\nnet::reply tiles::operator()(net::route_request const& req, bool) const {\n  auto const url = boost::url_view{req.target()};\n  if (url.path().starts_with(\"/tiles/glyphs\")) {\n    std::string decoded;\n    net::url_decode(url.path(), decoded);\n\n    // Rewrite old font name \"Noto Sans Display Regular\" to \"Noto Sans Regular\".\n    constexpr auto kDisplay = \" Display\"sv;\n    auto res_name = decoded.substr(14);\n    if (auto const display_pos = res_name.find(kDisplay);\n        display_pos != std::string::npos) {\n      res_name.erase(display_pos, kDisplay.length());\n    }\n\n    try {\n      auto const mem = pbf_sdf_fonts_res::get_resource(res_name);\n      auto res = net::web_server::string_res_t{boost::beast::http::status::ok,\n                                               req.version()};\n      res.body() =\n          std::string_view{reinterpret_cast<char const*>(mem.ptr_), mem.size_};\n      res.insert(boost::beast::http::field::content_type,\n                 \"application/x-protobuf\");\n      res.keep_alive(req.keep_alive());\n      return res;\n    } catch (std::out_of_range const&) {\n      throw net::not_found_exception{res_name};\n    }\n  }\n\n  auto const tile = ::tiles::parse_tile_url(url.path());\n  if (!tile.has_value()) {\n    return net::web_server::empty_res_t{boost::beast::http::status::not_found,\n                                        req.version()};\n  }\n\n  auto pc = ::tiles::null_perf_counter{};\n  auto const rendered_tile =\n      ::tiles::get_tile(tiles_data_.db_handle_, tiles_data_.pack_handle_,\n                        tiles_data_.render_ctx_, *tile, pc);\n\n  auto res = net::web_server::string_res_t{boost::beast::http::status::ok,\n                                           req.version()};\n  res.insert(boost::beast::http::field::content_type,\n             \"application/vnd.mapbox-vector-tile\");\n  res.insert(boost::beast::http::field::content_encoding, \"deflate\");\n  res.body() = rendered_tile.value_or(\"\");\n  res.keep_alive(req.keep_alive());\n  return res;\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/transfers.cc",
    "content": "#include \"motis/endpoints/transfers.h\"\n\n#include \"osr/geojson.h\"\n#include \"osr/routing/route.h\"\n\n#include \"utl/pipes/all.h\"\n#include \"utl/pipes/transform.h\"\n#include \"utl/pipes/vec.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/elevators/match_elevator.h\"\n#include \"motis/get_loc.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace json = boost::json;\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\napi::transfers_response transfers::operator()(\n    boost::urls::url_view const& url) const {\n  auto const q = motis::api::transfers_params{url.params()};\n  auto const rt = std::atomic_load(&rt_);\n  auto const e = rt->e_.get();\n  auto const l = tags_.get_location(tt_, q.id_);\n\n  auto const neighbors =\n      loc_rtree_.in_radius(tt_.locations_.coordinates_[l], kMaxDistance);\n\n  auto footpaths = hash_map<n::location_idx_t, api::Transfer>{};\n\n  for (auto const fp : tt_.locations_.footpaths_out_[0].at(l)) {\n    footpaths[fp.target()].default_ = fp.duration().count();\n  }\n  if (!tt_.locations_.footpaths_out_[n::kFootProfile].empty()) {\n    for (auto const fp : tt_.locations_.footpaths_out_[n::kFootProfile].at(l)) {\n      footpaths[fp.target()].foot_ = fp.duration().count();\n    }\n  }\n  if (!tt_.locations_.footpaths_out_[n::kWheelchairProfile].empty()) {\n    for (auto const fp :\n         tt_.locations_.footpaths_out_[n::kWheelchairProfile].at(l)) {\n      footpaths[fp.target()].wheelchair_ = fp.duration().count();\n    }\n  }\n  if (!tt_.locations_.footpaths_out_[n::kCarProfile].empty()) {\n    for (auto const fp : tt_.locations_.footpaths_out_[n::kCarProfile].at(l)) {\n      footpaths[fp.target()].car_ = fp.duration().count();\n    }\n  }\n\n  auto const loc = get_loc(tt_, w_, pl_, matches_, l);\n  for (auto const mode :\n       {osr::search_profile::kFoot, osr::search_profile::kWheelchair}) {\n    auto const results = osr::route(\n        to_profile_parameters(mode, {}), w_, l_, mode, loc,\n        utl::to_vec(\n            neighbors,\n            [&](auto&& l) { return get_loc(tt_, w_, pl_, matches_, l); }),\n        c_.timetable_.value().max_footpath_length_ * 60U,\n        osr::direction::kForward, c_.timetable_.value().max_matching_distance_,\n        e == nullptr ? nullptr : &e->blocked_, nullptr, nullptr,\n        [](osr::path const& p) { return p.uses_elevator_; });\n\n    for (auto const [n, r] : utl::zip(neighbors, results)) {\n      if (r.has_value()) {\n        auto& fp = footpaths[n];\n        auto const duration = std::ceil(r->cost_ / 60U);\n        if (duration < n::footpath::kMaxDuration.count()) {\n          switch (mode) {\n            case osr::search_profile::kFoot: fp.footRouted_ = duration; break;\n            case osr::search_profile::kWheelchair:\n              fp.wheelchairRouted_ = duration;\n              fp.wheelchairUsesElevator_ = r->uses_elevator_;\n              break;\n            default: std::unreachable();\n          }\n        }\n      }\n    }\n  }\n\n  auto const to_place = [&](n::location_idx_t const l) -> api::Place {\n    return {\n        .name_ =\n            std::string{tt_.get_default_translation(tt_.locations_.names_[l])},\n        .stopId_ = std::string{tt_.locations_.ids_[l].view()},\n        .lat_ = tt_.locations_.coordinates_[l].lat(),\n        .lon_ = tt_.locations_.coordinates_[l].lng(),\n        .level_ = pl_.get_level(w_, matches_[l]).to_float(),\n        .vertexType_ = api::VertexTypeEnum::NORMAL};\n  };\n\n  return {.place_ = to_place(l),\n          .root_ = to_place(tt_.locations_.get_root_idx(l)),\n          .equivalences_ = utl::to_vec(\n              tt_.locations_.equivalences_[l],\n              [&](n::location_idx_t const eq) { return to_place(eq); }),\n          .hasFootTransfers_ =\n              !tt_.locations_.footpaths_out_[n::kFootProfile].empty(),\n          .hasWheelchairTransfers_ =\n              !tt_.locations_.footpaths_out_[n::kWheelchairProfile].empty(),\n          .hasCarTransfers_ =\n              !tt_.locations_.footpaths_out_[n::kCarProfile].empty(),\n          .transfers_ = utl::to_vec(footpaths, [&](auto&& e) {\n            e.second.to_ = to_place(e.first);\n            return e.second;\n          })};\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/trip.cc",
    "content": "#include \"motis/endpoints/trip.h\"\n\n#include <chrono>\n\n#include \"net/not_found_exception.h\"\n\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/gtfsrt_resolve_run.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/data.h\"\n#include \"motis/gbfs/routing_data.h\"\n#include \"motis/journey_to_response.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/server.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\napi::Itinerary trip::operator()(boost::urls::url_view const& url) const {\n  auto const rt = std::atomic_load(&rt_);\n  auto const rtt = rt->rtt_.get();\n\n  auto query = api::trip_params{url.params()};\n  auto const api_version = get_api_version(url);\n\n  auto const [r, _] = tags_.get_trip(tt_, rtt, query.tripId_);\n  utl::verify<net::not_found_exception>(r.valid(),\n                                        \"trip not found: tripId={}, tt={}\",\n                                        query.tripId_, tt_.external_interval());\n\n  auto fr = n::rt::frun{tt_, rtt, r};\n  fr.stop_range_.to_ = fr.size();\n  fr.stop_range_.from_ = 0U;\n  auto const from_l = fr[0];\n  auto const to_l = fr[fr.size() - 1U];\n  auto const start_time = from_l.time(n::event_type::kDep);\n  auto const dest_time = to_l.time(n::event_type::kArr);\n  auto cache = street_routing_cache_t{};\n  auto blocked = osr::bitvec<osr::node_idx_t>{};\n  auto gbfs_rd = gbfs::gbfs_routing_data{};\n\n  return journey_to_response(\n      w_, l_, pl_, tt_, tags_, nullptr, nullptr, rtt, matches_, nullptr,\n      shapes_, gbfs_rd, ae_, tz_,\n      {.legs_ = {n::routing::journey::leg{\n           n::direction::kForward, from_l.get_location_idx(),\n           to_l.get_location_idx(), start_time, dest_time,\n           n::routing::journey::run_enter_exit{\n               fr,  // NOLINT(cppcoreguidelines-slicing)\n               fr.stop_range_.from_,\n               static_cast<n::stop_idx_t>(fr.stop_range_.to_ - 1U)}}},\n       .start_time_ = start_time,\n       .dest_time_ = dest_time,\n       .dest_ = to_l.get_location_idx(),\n       .transfers_ = 0U},\n      tt_location{from_l.get_location_idx(),\n                  from_l.get_scheduled_location_idx()},\n      tt_location{to_l.get_location_idx()}, cache, &blocked, false,\n      osr_parameters{}, api::PedestrianProfileEnum::FOOT,\n      api::ElevationCostsEnum::NONE, query.joinInterlinedLegs_, true,\n      query.detailedLegs_, false, query.withScheduledSkippedStops_,\n      config_.timetable_.value().max_matching_distance_, kMaxMatchingDistance,\n      api_version, false, false, query.language_);\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/endpoints/update_elevator.cc",
    "content": "#include \"motis/endpoints/update_elevator.h\"\n\n#include \"net/not_found_exception.h\"\n\n#include \"nigiri/rt/create_rt_timetable.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/elevators/parse_fasta.h\"\n#include \"motis/get_loc.h\"\n#include \"motis/railviz.h\"\n#include \"motis/update_rtt_td_footpaths.h\"\n\nnamespace json = boost::json;\nnamespace n = nigiri;\n\nnamespace motis::ep {\n\njson::value update_elevator::operator()(json::value const& query) const {\n  auto const& q = query.as_object();\n  auto const id = q.at(\"id\").to_number<std::int64_t>();\n  auto const new_status = q.at(\"status\").as_string() != \"INACTIVE\";\n  auto const new_out_of_service = parse_out_of_service(q);\n\n  auto const rt_copy = std::atomic_load(&rt_);\n  auto const e = rt_copy->e_.get();\n  utl::verify<net::not_found_exception>(e != nullptr,\n                                        \"elevators not available\");\n\n  auto const rtt = rt_copy->rtt_.get();\n  auto elevators_copy = e->elevators_;\n  auto const it =\n      utl::find_if(elevators_copy, [&](auto&& x) { return x.id_ == id; });\n  utl::verify<net::not_found_exception>(it != end(elevators_copy),\n                                        \"id {} not found\", id);\n\n  it->status_ = new_status;\n  it->out_of_service_ = new_out_of_service;\n  it->state_changes_ =\n      intervals_to_state_changes(it->out_of_service_, it->status_);\n\n  auto tasks = hash_set<std::pair<n::location_idx_t, osr::direction>>{};\n  loc_rtree_.in_radius(it->pos_, kElevatorUpdateRadius,\n                       [&](n::location_idx_t const l) {\n                         tasks.emplace(l, osr::direction::kForward);\n                         tasks.emplace(l, osr::direction::kBackward);\n                       });\n\n  auto new_e =\n      elevators{w_, elevator_ids_, elevator_nodes_, std::move(elevators_copy)};\n  auto new_rtt = n::rt::create_rt_timetable(tt_, rtt->base_day_);\n  update_rtt_td_footpaths(\n      w_, l_, pl_, tt_, loc_rtree_, new_e, matches_, tasks, rtt, new_rtt,\n      std::chrono::seconds{c_.timetable_.value().max_footpath_length_ * 60});\n\n  auto new_rt = std::make_shared<rt>(\n      std::make_unique<n::rt_timetable>(std::move(new_rtt)),\n      std::make_unique<elevators>(std::move(new_e)),\n      std::move(rt_copy->railviz_rt_));\n  std::atomic_store(&rt_, std::move(new_rt));\n\n  return json::string{{\"success\", true}};\n}\n\n}  // namespace motis::ep\n"
  },
  {
    "path": "src/flex/flex.cc",
    "content": "#include \"motis/flex/flex.h\"\n\n#include <ranges>\n\n#include \"utl/concat.h\"\n\n#include \"osr/lookup.h\"\n#include \"osr/routing/parameters.h\"\n#include \"osr/routing/profiles/foot.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/ways.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/flex/flex_areas.h\"\n#include \"motis/flex/flex_routing_data.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/max_distance.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::flex {\n\nosr::sharing_data prepare_sharing_data(n::timetable const& tt,\n                                       osr::ways const& w,\n                                       osr::lookup const& lookup,\n                                       osr::platforms const* pl,\n                                       flex_areas const& fa,\n                                       platform_matches_t const* pl_matches,\n                                       mode_id const id,\n                                       osr::direction const dir,\n                                       flex_routing_data& frd) {\n  auto const stop_seq =\n      tt.flex_stop_seq_[tt.flex_transport_stop_seq_[id.get_flex_transport()]];\n  auto const from_stop = stop_seq.at(id.get_stop());\n  auto to_stops = std::vector<n::flex_stop_t>{};\n  for (auto i = static_cast<int>(id.get_stop()) +\n                (dir == osr::direction::kForward ? 1 : -1);\n       dir == osr::direction::kForward ? i < static_cast<int>(stop_seq.size())\n                                       : i >= 0;\n       dir == osr::direction::kForward ? ++i : --i) {\n    to_stops.emplace_back(stop_seq.at(static_cast<n::stop_idx_t>(i)));\n  }\n\n  // Count additional nodes and allocate bit vectors.\n  auto n_nodes = w.n_nodes();\n  from_stop.apply(utl::overloaded{[&](n::location_group_idx_t const from_lg) {\n    n_nodes += tt.location_group_locations_[from_lg].size();\n  }});\n  for (auto const& to_stop : to_stops) {\n    to_stop.apply(utl::overloaded{[&](n::location_group_idx_t const to_lg) {\n      n_nodes += tt.location_group_locations_[to_lg].size();\n    }});\n  }\n  frd.additional_node_offset_ = w.n_nodes();\n  frd.additional_node_coordinates_.clear();\n  frd.additional_edges_.clear();\n  frd.start_allowed_.resize(n_nodes);\n  frd.end_allowed_.resize(n_nodes);\n  frd.through_allowed_.resize(n_nodes);\n  frd.start_allowed_.zero_out();\n  frd.end_allowed_.zero_out();\n  frd.through_allowed_.one_out();\n\n  // Creates an additional node for the given timetable location\n  // and adds additional edges to/from this node.\n  auto next_add_node_idx = osr::node_idx_t{w.n_nodes()};\n  auto const add_tt_location = [&](n::location_idx_t const l) {\n    frd.additional_nodes_.emplace_back(l);\n    frd.additional_node_coordinates_.emplace_back(\n        tt.locations_.coordinates_[l]);\n\n    auto const pos = get_location(&tt, &w, pl, pl_matches, tt_location{l});\n    auto const l_additional_node_idx = next_add_node_idx++;\n\n    auto const matches = lookup.match<osr::foot<false>>(\n        osr::foot<false>::parameters{}, pos, false, osr::direction::kForward,\n        kMaxGbfsMatchingDistance, nullptr);\n\n    for (auto const& m : matches) {\n      auto const handle_node = [&](osr::node_candidate const& node) {\n        if (!node.valid() || node.dist_to_node_ > kMaxGbfsMatchingDistance) {\n          return;\n        }\n\n        auto const edge_to_an = osr::additional_edge{\n            l_additional_node_idx,\n            static_cast<osr::distance_t>(node.dist_to_node_)};\n        auto& node_edges = frd.additional_edges_[node.node_];\n        if (utl::find(node_edges, edge_to_an) == end(node_edges)) {\n          node_edges.emplace_back(edge_to_an);\n        }\n\n        auto& add_node_out = frd.additional_edges_[l_additional_node_idx];\n        auto const edge_from_an = osr::additional_edge{\n            node.node_, static_cast<osr::distance_t>(node.dist_to_node_)};\n        if (utl::find(add_node_out, edge_from_an) == end(add_node_out)) {\n          add_node_out.emplace_back(edge_from_an);\n        }\n      };\n\n      handle_node(m.left_);\n      handle_node(m.right_);\n    }\n\n    return l_additional_node_idx;\n  };\n\n  // Set start allowed in start area / location group.\n  auto tmp = osr::bitvec<osr::node_idx_t>{};\n  from_stop.apply(utl::overloaded{\n      [&](n::location_group_idx_t const from_lg) {\n        for (auto const& l : tt.location_group_locations_[from_lg]) {\n          frd.start_allowed_.set(add_tt_location(l), true);\n        }\n      },\n      [&](n::flex_area_idx_t const from_area) {\n        fa.add_area(from_area, frd.start_allowed_, tmp);\n      }});\n\n  // Set end allowed in follow-up areas / location groups.\n  for (auto const& to_stop : to_stops) {\n    to_stop.apply(utl::overloaded{\n        [&](n::location_group_idx_t const to_lg) {\n          for (auto const& l : tt.location_group_locations_[to_lg]) {\n            frd.end_allowed_.set(add_tt_location(l), true);\n          }\n        },\n        [&](n::flex_area_idx_t const to_area) {\n          fa.add_area(to_area, frd.end_allowed_, tmp);\n        }});\n  }\n\n  return frd.to_sharing_data();\n}\n\nn::interval<n::day_idx_t> get_relevant_days(\n    n::timetable const& tt, n::routing::start_time_t const start_time) {\n  auto const to_sys_days = [](n::unixtime_t const t) {\n    return std::chrono::time_point_cast<date::sys_days::duration>(t);\n  };\n  auto const iv = std::visit(\n      utl::overloaded{[&](n::unixtime_t const t) {\n                        return n::interval{to_sys_days(t) - date::days{2},\n                                           to_sys_days(t) + date::days{3}};\n                      },\n                      [&](n::interval<n::unixtime_t> const x) {\n                        return n::interval{to_sys_days(x.from_) - date::days{2},\n                                           to_sys_days(x.to_) + date::days{3}};\n                      }},\n      start_time);\n  return n::interval{tt.day_idx(iv.from_), tt.day_idx(iv.to_)};\n}\n\nflex_routings_t get_flex_routings(\n    n::timetable const& tt,\n    point_rtree<n::location_idx_t> const& loc_rtree,\n    n::routing::start_time_t const start_time,\n    geo::latlng const& pos,\n    osr::direction const dir,\n    std::chrono::seconds const max) {\n  auto routings = flex_routings_t{};\n\n  // Traffic days helpers.\n  auto const day_idx_iv = get_relevant_days(tt, start_time);\n  auto const is_active = [&](n::flex_transport_idx_t const t) {\n    auto const& bitfield = tt.bitfields_[tt.flex_transport_traffic_days_[t]];\n    return utl::any_of(day_idx_iv, [&](n::day_idx_t const i) {\n      return bitfield.test(to_idx(i));\n    });\n  };\n\n  // Stop index helper.\n  auto const get_stop_idx =\n      [&](n::flex_stop_seq_idx_t const stop_seq_idx,\n          n::flex_stop_t const x) -> std::optional<n::stop_idx_t> {\n    auto const stops = tt.flex_stop_seq_[stop_seq_idx];\n    auto const is_last = [&](n::stop_idx_t const stop_idx) {\n      return (dir == osr::direction::kBackward && stop_idx == 0U) ||\n             (dir == osr::direction::kForward && stop_idx == stops.size() - 1U);\n    };\n    for (auto c = 0U; c != stops.size(); ++c) {\n      auto const stop_idx = static_cast<n::stop_idx_t>(\n          dir == osr::direction::kForward ? c : stops.size() - c - 1);\n      if (stops[stop_idx] == x && !is_last(stop_idx)) {\n        return stop_idx;\n      }\n    }\n    return std::nullopt;\n  };\n\n  // Collect area transports.\n  auto const add_area_flex_transports = [&](n::flex_area_idx_t const a) {\n    for (auto const t : tt.flex_area_transports_[a]) {\n      if (!is_active(t)) {\n        continue;\n      }\n\n      auto const stop_idx = get_stop_idx(tt.flex_transport_stop_seq_[t], a);\n      if (stop_idx.has_value()) {\n        routings[std::pair{tt.flex_transport_stop_seq_[t], *stop_idx}]\n            .emplace_back(t, *stop_idx, dir);\n      }\n    }\n  };\n  auto const box =\n      geo::box{pos, get_max_distance(osr::search_profile::kFoot, max)};\n  tt.flex_area_rtree_.search(box.min_.lnglat_float(), box.max_.lnglat_float(),\n                             [&](auto&&, auto&&, n::flex_area_idx_t const a) {\n                               add_area_flex_transports(a);\n                               return true;\n                             });\n\n  // Collect location group transports.\n  auto location_groups = hash_set<n::location_group_idx_t>{};\n  loc_rtree.in_radius(pos, get_max_distance(osr::search_profile::kFoot, max),\n                      [&](n::location_idx_t const l) {\n                        for (auto const lg : tt.location_location_groups_[l]) {\n                          location_groups.emplace(lg);\n                        }\n                        return true;\n                      });\n  for (auto const& lg : location_groups) {\n    for (auto const t : tt.location_group_transports_[lg]) {\n      if (!is_active(t)) {\n        continue;\n      }\n\n      auto const stop_idx = get_stop_idx(tt.flex_transport_stop_seq_[t], lg);\n      if (stop_idx.has_value()) {\n        routings[std::pair{tt.flex_transport_stop_seq_[t], *stop_idx}]\n            .emplace_back(t, *stop_idx, dir);\n      }\n    }\n  }\n\n  return routings;\n}\n\nbool is_in_flex_stop(n::timetable const& tt,\n                     osr::ways const& w,\n                     flex_areas const& fa,\n                     flex_routing_data const& frd,\n                     n::flex_stop_t const& s,\n                     osr::node_idx_t const n) {\n  return s.apply(utl::overloaded{\n      [&](n::flex_area_idx_t const a) {\n        return !w.is_additional_node(n) && n != osr::node_idx_t::invalid() &&\n               fa.is_in_area(a, w.get_node_pos(n));\n      },\n      [&](n::location_group_idx_t const lg) {\n        if (!w.is_additional_node(n)) {\n          return false;\n        }\n        auto const locations = tt.location_group_locations_.at(lg);\n        auto const l = frd.get_additional_node(n);\n        return utl::find(locations, l) != end(locations);\n      }});\n}\n\nvoid add_flex_td_offsets(osr::ways const& w,\n                         osr::lookup const& lookup,\n                         osr::platforms const* pl,\n                         platform_matches_t const* matches,\n                         way_matches_storage const* way_matches,\n                         n::timetable const& tt,\n                         flex_areas const& fa,\n                         point_rtree<n::location_idx_t> const& loc_rtree,\n                         n::routing::start_time_t const start_time,\n                         osr::location const& pos,\n                         osr::direction const dir,\n                         std::chrono::seconds const max,\n                         double const max_matching_distance,\n                         osr_parameters const& osr_params,\n                         flex_routing_data& frd,\n                         n::routing::td_offsets_t& ret,\n                         std::map<std::string, std::uint64_t>& stats) {\n  UTL_START_TIMING(flex_lookup_timer);\n\n  auto const max_dist = get_max_distance(osr::search_profile::kCarSharing, max);\n  auto const near_stops = loc_rtree.in_radius(pos.pos_, max_dist);\n  auto const near_stop_locations =\n      utl::to_vec(near_stops, [&](n::location_idx_t const l) {\n        return get_location(&tt, &w, pl, matches, tt_location{l});\n      });\n\n  auto const params =\n      to_profile_parameters(osr::search_profile::kCarSharing, osr_params);\n  auto const pos_match =\n      lookup.match(params, pos, false, dir, max_matching_distance, nullptr,\n                   osr::search_profile::kCarSharing);\n  auto const near_stop_matches = get_reverse_platform_way_matches(\n      lookup, way_matches, osr::search_profile::kCarSharing, near_stops,\n      near_stop_locations, dir, max_matching_distance);\n\n  auto const routings =\n      get_flex_routings(tt, loc_rtree, start_time, pos.pos_, dir, max);\n\n  stats.emplace(fmt::format(\"prepare_{}_FLEX_lookup\", to_str(dir)),\n                UTL_GET_TIMING_MS(flex_lookup_timer));\n\n  for (auto const& [stop_seq, transports] : routings) {\n    UTL_START_TIMING(routing_timer);\n\n    auto const sharing_data = prepare_sharing_data(\n        tt, w, lookup, pl, fa, matches, transports.front(), dir, frd);\n\n    auto const paths =\n        osr::route(params, w, lookup, osr::search_profile::kCarSharing, pos,\n                   near_stop_locations, pos_match, near_stop_matches,\n                   static_cast<osr::cost_t>(max.count()), dir, nullptr,\n                   &sharing_data, nullptr);\n    auto const day_idx_iv = get_relevant_days(tt, start_time);\n    for (auto const id : transports) {\n      auto const t = id.get_flex_transport();\n      auto const from_stop_idx = id.get_stop();\n\n      for (auto const day_idx : day_idx_iv) {\n        if (!tt.bitfields_[tt.flex_transport_traffic_days_[t]].test(\n                to_idx(day_idx))) {\n          continue;\n        }\n\n        auto const day =\n            tt.internal_interval().from_ + to_idx(day_idx) * date::days{1U};\n        auto const from_stop_time_window =\n            tt.flex_transport_stop_time_windows_[t][from_stop_idx];\n        auto const abs_from_stop_iv = n::interval{\n            day + from_stop_time_window.from_, day + from_stop_time_window.to_};\n        for (auto const [p, s, l] :\n             utl::zip(paths, near_stop_locations, near_stops)) {\n          if (p.has_value()) {\n            auto const rel_to_stop_idx = 0U;\n            auto const to_stop_idx = static_cast<n::stop_idx_t>(\n                dir == osr::direction::kForward\n                    ? from_stop_idx + rel_to_stop_idx\n                    : from_stop_idx - rel_to_stop_idx);\n            auto const duration = n::duration_t{p->cost_ / 60};\n            auto const to_stop_time_window =\n                tt.flex_transport_stop_time_windows_[t][to_stop_idx];\n            auto const abs_to_stop_iv = n::interval{\n                day + to_stop_time_window.from_, day + to_stop_time_window.to_};\n\n            auto const iv_at_to_stop =\n                (dir == osr::direction::kForward ? abs_from_stop_iv >> duration\n                                                 : abs_from_stop_iv << duration)\n                    .intersect(abs_to_stop_iv);\n            auto const iv_at_from_stop = dir == osr::direction::kForward\n                                             ? iv_at_to_stop << duration\n                                             : iv_at_to_stop >> duration;\n\n            auto& offsets = ret[l];\n            if (offsets.empty()) {\n              offsets.emplace_back(n::unixtime_t{n::i32_minutes{0U}},\n                                   n::footpath::kMaxDuration, id.to_id());\n            }\n            offsets.emplace_back(iv_at_from_stop.from_, duration, id.to_id());\n            offsets.emplace_back(iv_at_from_stop.to_, n::footpath::kMaxDuration,\n                                 id.to_id());\n          }\n        }\n      }\n    }\n\n    stats.emplace(\n        fmt::format(\"prepare_{}_FLEX_{}\", to_str(dir),\n                    tt.flex_stop_seq_[stop_seq.first][stop_seq.second].apply(\n                        utl::overloaded{[&](n::location_group_idx_t const g) {\n                                          return tt.get_default_translation(\n                                              tt.location_group_name_[g]);\n                                        },\n                                        [&](n::flex_area_idx_t const a) {\n                                          return tt.get_default_translation(\n                                              tt.flex_area_name_[a]);\n                                        }})),\n        UTL_GET_TIMING_MS(routing_timer));\n  }\n\n  for (auto& [_, offsets] : ret) {\n    utl::sort(offsets, [](n::routing::td_offset const& a,\n                          n::routing::td_offset const& b) {\n      return a.valid_from_ < b.valid_from_;\n    });\n  }\n}\n\n}  // namespace motis::flex\n"
  },
  {
    "path": "src/flex/flex_areas.cc",
    "content": "#include \"motis/flex/flex_areas.h\"\n\n#include \"osr/lookup.h\"\n\n#include \"utl/concat.h\"\n#include \"utl/parallel_for.h\"\n\n#include \"nigiri/timetable.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::flex {\n\ntg_ring* convert_ring(std::vector<tg_point>& ring_tmp, auto&& osm_ring) {\n  ring_tmp.clear();\n  for (auto const& p : osm_ring) {\n    ring_tmp.emplace_back(tg_point{p.lng_, p.lat_});\n  }\n  return tg_ring_new(ring_tmp.data(), static_cast<int>(ring_tmp.size()));\n}\n\nflex_areas::~flex_areas() {\n  for (auto const& mp : idx_) {\n    tg_geom_free(mp);\n  }\n}\n\nflex_areas::flex_areas(nigiri::timetable const& tt,\n                       osr::ways const& w,\n                       osr::lookup const& l) {\n  struct tmp {\n    std::vector<tg_point> ring_tmp_;\n    std::vector<tg_ring*> inner_tmp_;\n    std::vector<tg_poly*> polys_tmp_;\n    basic_string<n::flex_area_idx_t> areas_;\n  };\n\n  area_nodes_.resize(tt.flex_area_outers_.size());\n  idx_.resize(tt.flex_area_outers_.size());\n  utl::parallel_for_run_threadlocal<tmp>(\n      tt.flex_area_outers_.size(), [&](tmp& tmp, std::size_t const i) {\n        tmp.polys_tmp_.clear();\n\n        auto const a = n::flex_area_idx_t{i};\n        auto const& outer_rings = tt.flex_area_outers_[a];\n\n        auto box = geo::box{};\n        for (auto const [outer_idx, outer_ring] : utl::enumerate(outer_rings)) {\n          tmp.inner_tmp_.clear();\n          for (auto const inner_ring :\n               tt.flex_area_inners_[a][static_cast<unsigned>(outer_idx)]) {\n            tmp.inner_tmp_.emplace_back(\n                convert_ring(tmp.ring_tmp_, inner_ring));\n          }\n\n          for (auto const& c : outer_ring) {\n            box.extend(c);\n          }\n\n          auto const outer = convert_ring(tmp.ring_tmp_, outer_ring);\n          auto const poly =\n              tg_poly_new(outer, tmp.inner_tmp_.data(),\n                          static_cast<int>(tmp.inner_tmp_.size()));\n          tg_ring_free(outer);\n          tmp.polys_tmp_.emplace_back(poly);\n        }\n\n        idx_[a] = tg_geom_new_multipolygon(\n            tmp.polys_tmp_.data(), static_cast<int>(tmp.polys_tmp_.size()));\n        auto b = osr::bitvec<osr::node_idx_t>{};\n        b.resize(w.n_nodes());\n        l.find(tt.flex_area_bbox_[a], [&](osr::way_idx_t const way) {\n          for (auto const& x : w.r_->way_nodes_[way]) {\n            if (is_in_area(a, w.get_node_pos(x).as_latlng())) {\n              b.set(x, true);\n            }\n          }\n        });\n\n        area_nodes_[a] = gbfs::compress_bitvec(b);\n\n        for (auto const& x : tmp.inner_tmp_) {\n          tg_ring_free(x);\n        }\n\n        for (auto const x : tmp.polys_tmp_) {\n          tg_poly_free(x);\n        }\n      });\n}\n\nbool flex_areas::is_in_area(nigiri::flex_area_idx_t const a,\n                            geo::latlng const& c) const {\n  auto const point = tg_geom_new_point(tg_point{c.lng(), c.lat()});\n  auto const result = tg_geom_within(point, idx_[a]);\n  tg_geom_free(point);\n  return result;\n}\n\nvoid flex_areas::add_area(nigiri::flex_area_idx_t a,\n                          osr::bitvec<osr::node_idx_t>& b,\n                          osr::bitvec<osr::node_idx_t>& tmp) const {\n  gbfs::decompress_bitvec(area_nodes_[a], tmp);\n  tmp.for_each_set_bit([&](auto&& i) { b.set(osr::node_idx_t{i}, true); });\n}\n\n}  // namespace motis::flex"
  },
  {
    "path": "src/flex/flex_output.cc",
    "content": "#include \"motis/flex/flex_output.h\"\n\n#include \"nigiri/flex.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/flex/flex.h\"\n#include \"motis/flex/flex_areas.h\"\n#include \"motis/flex/flex_routing_data.h\"\n#include \"motis/osr/street_routing.h\"\n#include \"motis/place.h\"\n\nnamespace n = nigiri;\n\nnamespace motis::flex {\n\nstd::string_view get_flex_stop_name(n::timetable const& tt,\n                                    n::lang_t const& lang,\n                                    n::flex_stop_t const& s) {\n  return s.apply(\n      utl::overloaded{[&](n::flex_area_idx_t const a) {\n                        return tt.translate(lang, tt.flex_area_name_[a]);\n                      },\n                      [&](n::location_group_idx_t const lg) {\n                        return tt.translate(lang, tt.location_group_name_[lg]);\n                      }});\n}\n\nstd::string_view get_flex_id(n::timetable const& tt, n::flex_stop_t const& s) {\n  return s.apply(utl::overloaded{[&](n::flex_area_idx_t const a) {\n                                   return tt.strings_.get(tt.flex_area_id_[a]);\n                                 },\n                                 [&](n::location_group_idx_t const lg) {\n                                   return tt.strings_.get(\n                                       tt.location_group_id_[lg]);\n                                 }});\n}\n\nflex_output::flex_output(osr::ways const& w,\n                         osr::lookup const& l,\n                         osr::platforms const* pl,\n                         platform_matches_t const* matches,\n                         adr_ext const* ae,\n                         tz_map_t const* tz,\n                         tag_lookup const& tags,\n                         n::timetable const& tt,\n                         flex_areas const& fa,\n                         mode_id const id)\n    : w_{w},\n      pl_{pl},\n      matches_{matches},\n      ae_{ae},\n      tz_{tz},\n      tt_{tt},\n      tags_{tags},\n      fa_{fa},\n      sharing_data_{flex::prepare_sharing_data(\n          tt, w, l, pl, fa, matches, id, id.get_dir(), flex_routing_data_)},\n      mode_id_(id) {}\n\nflex_output::~flex_output() = default;\n\nbool flex_output::is_time_dependent() const { return false; }\n\napi::ModeEnum flex_output::get_mode() const { return api::ModeEnum::FLEX; }\n\ntransport_mode_t flex_output::get_cache_key() const { return mode_id_.to_id(); }\n\nosr::search_profile flex_output::get_profile() const {\n  return osr::search_profile::kCarSharing;\n}\n\nosr::sharing_data const* flex_output::get_sharing_data() const {\n  return &sharing_data_;\n}\n\nvoid flex_output::annotate_leg(n::lang_t const& lang,\n                               osr::node_idx_t const from,\n                               osr::node_idx_t const to,\n                               api::Leg& leg) const {\n  if (from == osr::node_idx_t::invalid() || to == osr::node_idx_t::invalid()) {\n    return;\n  }\n\n  auto const t = mode_id_.get_flex_transport();\n  auto const stop_seq = tt_.flex_stop_seq_[tt_.flex_transport_stop_seq_[t]];\n  auto from_stop = std::optional<n::stop_idx_t>{};\n  auto to_stop = std::optional<n::stop_idx_t>{};\n  for (auto i = 0U; i != stop_seq.size(); ++i) {\n    auto const stop_idx = static_cast<n::stop_idx_t>(\n        mode_id_.get_dir() == osr::direction::kForward\n            ? i\n            : stop_seq.size() - i - 1U);\n    auto const stop = stop_seq[stop_idx];\n    if (!from_stop.has_value() &&\n        is_in_flex_stop(tt_, w_, fa_, flex_routing_data_, stop, from)) {\n      from_stop = stop_idx;\n    } else if (!to_stop.has_value() &&\n               is_in_flex_stop(tt_, w_, fa_, flex_routing_data_, stop, to)) {\n      to_stop = stop_idx;\n      break;\n    }\n  }\n\n  if (!from_stop.has_value()) {\n    n::log(n::log_lvl::error, \"flex\", \"flex: from  [node={}] not found\", from);\n    return;\n  }\n\n  if (!to_stop.has_value()) {\n    n::log(n::log_lvl::error, \"flex\", \"flex: to [node={}] not found\", to);\n    return;\n  }\n\n  auto const write_node_info = [&](api::Place& p, osr::node_idx_t const n) {\n    if (w_.is_additional_node(n)) {\n      auto const l = flex_routing_data_.get_additional_node(n);\n      p = to_place(&tt_, &tags_, &w_, pl_, matches_, ae_, tz_, lang,\n                   tt_location{l});\n    }\n  };\n  write_node_info(leg.from_, from);\n  write_node_info(leg.to_, to);\n\n  leg.mode_ = api::ModeEnum::FLEX;\n  leg.from_.flex_ = get_flex_stop_name(tt_, lang, stop_seq[*from_stop]);\n  leg.from_.flexId_ = get_flex_id(tt_, stop_seq[*from_stop]);\n  leg.to_.flex_ = get_flex_stop_name(tt_, lang, stop_seq[*to_stop]);\n  leg.to_.flexId_ = get_flex_id(tt_, stop_seq[*to_stop]);\n\n  auto const time_windows = tt_.flex_transport_stop_time_windows_[t];\n\n  leg.from_.flexStartPickupDropOffWindow_ =\n      std::chrono::time_point_cast<std::chrono::days>(leg.startTime_.time_) +\n      time_windows[*from_stop].from_;\n  leg.from_.flexEndPickupDropOffWindow_ =\n      std::chrono::time_point_cast<std::chrono::days>(leg.startTime_.time_) +\n      time_windows[*from_stop].to_;\n\n  leg.to_.flexStartPickupDropOffWindow_ =\n      std::chrono::time_point_cast<std::chrono::days>(leg.endTime_.time_) +\n      time_windows[*to_stop].from_;\n  leg.to_.flexEndPickupDropOffWindow_ =\n      std::chrono::time_point_cast<std::chrono::days>(leg.endTime_.time_) +\n      time_windows[*to_stop].to_;\n}\n\napi::Place flex_output::get_place(n::lang_t const& lang,\n                                  osr::node_idx_t const n,\n                                  std::optional<std::string> const& tz) const {\n  if (w_.is_additional_node(n)) {\n    auto const l = flex_routing_data_.get_additional_node(n);\n    auto const c = tt_.locations_.coordinates_.at(l);\n    return api::Place{\n        .name_ = std::string{tt_.translate(lang, tt_.locations_.names_.at(l))},\n        .lat_ = c.lat_,\n        .lon_ = c.lng_,\n        .tz_ = tz,\n        .vertexType_ = api::VertexTypeEnum::TRANSIT};\n  } else {\n    auto const pos = w_.get_node_pos(n).as_latlng();\n    return api::Place{.lat_ = pos.lat_,\n                      .lon_ = pos.lng_,\n                      .tz_ = tz,\n                      .vertexType_ = api::VertexTypeEnum::NORMAL};\n  }\n}\n\n}  // namespace motis::flex"
  },
  {
    "path": "src/gbfs/data.cc",
    "content": "#include \"motis/gbfs/data.h\"\n\n#include \"osr/lookup.h\"\n#include \"osr/ways.h\"\n\n#include \"motis/gbfs/compression.h\"\n#include \"motis/gbfs/routing_data.h\"\n\nnamespace motis::gbfs {\n\nproducts_routing_data::products_routing_data(\n    std::shared_ptr<provider_routing_data const>&& prd,\n    compressed_routing_data const& compressed)\n    : provider_routing_data_{std::move(prd)}, compressed_{compressed} {\n  decompress_bitvec(compressed_.start_allowed_, start_allowed_);\n  decompress_bitvec(compressed_.end_allowed_, end_allowed_);\n  decompress_bitvec(compressed_.through_allowed_, through_allowed_);\n}\n\nstd::shared_ptr<products_routing_data> gbfs_data::get_products_routing_data(\n    osr::ways const& w,\n    osr::lookup const& l,\n    gbfs_products_ref const prod_ref) {\n  auto lock = std::unique_lock{products_routing_data_mutex_};\n\n  if (auto it = products_routing_data_.find(prod_ref);\n      it != end(products_routing_data_)) {\n    if (auto prod_rd = it->second.lock(); prod_rd) {\n      return prod_rd;\n    }\n  }\n\n  auto provider_rd = get_provider_routing_data(\n      w, l, *this, *providers_.at(prod_ref.provider_));\n  auto prod_rd = provider_rd->get_products_routing_data(prod_ref.products_);\n  products_routing_data_[prod_ref] = prod_rd;\n  return prod_rd;\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/gbfs_output.cc",
    "content": "#include \"motis/gbfs/gbfs_output.h\"\n\n#include \"motis/gbfs/mode.h\"\n#include \"motis/gbfs/osr_profile.h\"\n#include \"motis/gbfs/routing_data.h\"\n\nnamespace motis::gbfs {\n\ngbfs_output::~gbfs_output() = default;\n\ngbfs_output::gbfs_output(osr::ways const& w,\n                         gbfs_routing_data& gbfs_rd,\n                         gbfs_products_ref const prod_ref,\n                         bool const ignore_rental_return_constraints)\n    : w_{w},\n      gbfs_rd_{gbfs_rd},\n      provider_{*gbfs_rd_.data_->providers_.at(prod_ref.provider_)},\n      products_{provider_.products_.at(prod_ref.products_)},\n      prod_rd_{gbfs_rd_.get_products_routing_data(prod_ref)},\n      sharing_data_{prod_rd_->get_sharing_data(\n          w_.n_nodes(), ignore_rental_return_constraints)},\n      rental_{\n          .providerId_ = provider_.id_,\n          .providerGroupId_ = provider_.group_id_,\n          .systemId_ = provider_.sys_info_.id_,\n          .systemName_ = provider_.sys_info_.name_,\n          .url_ = provider_.sys_info_.url_,\n          .color_ = provider_.color_,\n          .formFactor_ = to_api_form_factor(products_.form_factor_),\n          .propulsionType_ = to_api_propulsion_type(products_.propulsion_type_),\n          .returnConstraint_ =\n              to_api_return_constraint(products_.return_constraint_)} {}\n\napi::ModeEnum gbfs_output::get_mode() const { return api::ModeEnum::RENTAL; }\n\nbool gbfs_output::is_time_dependent() const { return false; }\n\ntransport_mode_t gbfs_output::get_cache_key() const {\n  return gbfs_rd_.get_transport_mode({provider_.idx_, products_.idx_});\n}\n\nosr::search_profile gbfs_output::get_profile() const {\n  return get_osr_profile(products_.form_factor_);\n}\n\nosr::sharing_data const* gbfs_output::get_sharing_data() const {\n  return &sharing_data_;\n}\n\nvoid gbfs_output::annotate_leg(nigiri::lang_t const&,\n                               osr::node_idx_t const from_node,\n                               osr::node_idx_t const to_node,\n                               api::Leg& leg) const {\n  auto const from_additional_node = w_.is_additional_node(from_node);\n  auto const to_additional_node = w_.is_additional_node(to_node);\n  auto const is_rental =\n      (leg.mode_ == api::ModeEnum::BIKE || leg.mode_ == api::ModeEnum::CAR) &&\n      (from_additional_node || to_additional_node);\n  if (!is_rental) {\n    return;\n  }\n\n  leg.rental_ = rental_;\n  leg.mode_ = api::ModeEnum::RENTAL;\n  auto& ret = *leg.rental_;\n  if (w_.is_additional_node(from_node)) {\n    auto const& an = prod_rd_->compressed_.additional_nodes_.at(\n        get_additional_node_idx(from_node));\n    std::visit(\n        utl::overloaded{\n            [&](additional_node::station const& s) {\n              auto const& st = provider_.stations_.at(s.id_);\n              ret.fromStationName_ = st.info_.name_;\n              ret.stationName_ = st.info_.name_;\n              ret.rentalUriAndroid_ = st.info_.rental_uris_.android_;\n              ret.rentalUriIOS_ = st.info_.rental_uris_.ios_;\n              ret.rentalUriWeb_ = st.info_.rental_uris_.web_;\n            },\n            [&](additional_node::vehicle const& v) {\n              auto const& vs = provider_.vehicle_status_.at(v.idx_);\n              if (auto const st = provider_.stations_.find(vs.station_id_);\n                  st != end(provider_.stations_)) {\n                ret.fromStationName_ = st->second.info_.name_;\n              }\n              ret.rentalUriAndroid_ = vs.rental_uris_.android_;\n              ret.rentalUriIOS_ = vs.rental_uris_.ios_;\n              ret.rentalUriWeb_ = vs.rental_uris_.web_;\n            }},\n        an.data_);\n  }\n  if (w_.is_additional_node(to_node)) {\n    auto const& an = prod_rd_->compressed_.additional_nodes_.at(\n        get_additional_node_idx(to_node));\n    std::visit(\n        utl::overloaded{[&](additional_node::station const& s) {\n                          auto const& st = provider_.stations_.at(s.id_);\n                          ret.toStationName_ = st.info_.name_;\n                          if (!ret.stationName_) {\n                            ret.stationName_ = ret.toStationName_;\n                          }\n                        },\n                        [&](additional_node::vehicle const& v) {\n                          auto const& vs = provider_.vehicle_status_.at(v.idx_);\n                          if (auto const st =\n                                  provider_.stations_.find(vs.station_id_);\n                              st != end(provider_.stations_)) {\n                            ret.toStationName_ = st->second.info_.name_;\n                          }\n                        }},\n        an.data_);\n  }\n}\n\napi::Place gbfs_output::get_place(nigiri::lang_t const&,\n                                  osr::node_idx_t const n,\n                                  std::optional<std::string> const& tz) const {\n  if (w_.is_additional_node(n)) {\n    auto const pos = get_sharing_data()->get_additional_node_coordinates(n);\n    return api::Place{.name_ = get_node_name(n),\n                      .lat_ = pos.lat_,\n                      .lon_ = pos.lng_,\n                      .tz_ = tz,\n                      .vertexType_ = api::VertexTypeEnum::BIKESHARE};\n  } else {\n    auto const pos = w_.get_node_pos(n).as_latlng();\n    return api::Place{.lat_ = pos.lat_,\n                      .lon_ = pos.lng_,\n                      .tz_ = tz,\n                      .vertexType_ = api::VertexTypeEnum::NORMAL};\n  }\n}\n\nstd::string gbfs_output::get_node_name(osr::node_idx_t const n) const {\n  auto const& an =\n      prod_rd_->compressed_.additional_nodes_.at(get_additional_node_idx(n));\n  return std::visit(\n      utl::overloaded{[&](additional_node::station const& s) {\n                        return provider_.stations_.at(s.id_).info_.name_;\n                      },\n                      [&](additional_node::vehicle const& v) {\n                        auto const& vs = provider_.vehicle_status_.at(v.idx_);\n                        auto const it =\n                            provider_.stations_.find(vs.station_id_);\n                        return it == end(provider_.stations_)\n                                   ? provider_.sys_info_.name_\n                                   : it->second.info_.name_;\n                      }},\n      an.data_);\n}\n\nstd::size_t gbfs_output::get_additional_node_idx(\n    osr::node_idx_t const n) const {\n  return to_idx(n) - sharing_data_.additional_node_offset_;\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/geofencing.cc",
    "content": "#include \"motis/gbfs/data.h\"\n\n#include \"utl/helpers/algorithm.h\"\n\n#include \"tg.h\"\n\nnamespace motis::gbfs {\n\nbool applies(std::vector<vehicle_type_idx_t> const& rule_vehicle_type_idxs,\n             std::vector<vehicle_type_idx_t> const& segment_vehicle_type_idxs) {\n  return rule_vehicle_type_idxs.empty() ||\n         utl::all_of(segment_vehicle_type_idxs, [&](auto const& idx) {\n           return utl::find(rule_vehicle_type_idxs, idx) !=\n                  end(rule_vehicle_type_idxs);\n         });\n}\n\nbool multipoly_contains_point(tg_geom const* geom, geo::latlng const& pos) {\n  auto const n_polys = tg_geom_num_polys(geom);\n  for (auto i = 0; i < n_polys; ++i) {\n    auto const* poly = tg_geom_poly_at(geom, i);\n    if (tg_geom_intersects_xy(reinterpret_cast<tg_geom const*>(poly), pos.lng(),\n                              pos.lat())) {\n      return true;\n    }\n  }\n  return false;\n}\n\ngeofencing_restrictions geofencing_zones::get_restrictions(\n    geo::latlng const& pos,\n    vehicle_type_idx_t const vehicle_type_idx,\n    geofencing_restrictions const& default_restrictions) const {\n  auto const check_vehicle_type =\n      vehicle_type_idx != vehicle_type_idx_t::invalid();\n  for (auto const& z : zones_) {\n    if (multipoly_contains_point(z.geom_.get(), pos)) {\n      for (auto const& r : z.rules_) {\n        if (check_vehicle_type && !r.vehicle_type_idxs_.empty() &&\n            utl::find(r.vehicle_type_idxs_, vehicle_type_idx) ==\n                end(r.vehicle_type_idxs_)) {\n          continue;\n        }\n        return geofencing_restrictions{\n            .ride_start_allowed_ = r.ride_start_allowed_,\n            .ride_end_allowed_ = r.ride_end_allowed_,\n            .ride_through_allowed_ = r.ride_through_allowed_,\n            .station_parking_ = r.station_parking_};\n      }\n    }\n  }\n  return default_restrictions;\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/mode.cc",
    "content": "#include \"motis/gbfs/mode.h\"\n\n#include <utility>\n\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/verify.h\"\n\n#include \"motis/constants.h\"\n\nnamespace motis::gbfs {\n\napi::RentalFormFactorEnum to_api_form_factor(vehicle_form_factor const ff) {\n  switch (ff) {\n    case vehicle_form_factor::kBicycle:\n      return api::RentalFormFactorEnum::BICYCLE;\n    case vehicle_form_factor::kCargoBicycle:\n      return api::RentalFormFactorEnum::CARGO_BICYCLE;\n    case vehicle_form_factor::kCar: return api::RentalFormFactorEnum::CAR;\n    case vehicle_form_factor::kMoped: return api::RentalFormFactorEnum::MOPED;\n    case vehicle_form_factor::kScooterStanding:\n      return api::RentalFormFactorEnum::SCOOTER_STANDING;\n    case vehicle_form_factor::kScooterSeated:\n      return api::RentalFormFactorEnum::SCOOTER_SEATED;\n    case vehicle_form_factor::kOther: return api::RentalFormFactorEnum::OTHER;\n  }\n  std::unreachable();\n}\n\nvehicle_form_factor from_api_form_factor(api::RentalFormFactorEnum const ff) {\n  switch (ff) {\n    case api::RentalFormFactorEnum::BICYCLE:\n      return vehicle_form_factor::kBicycle;\n    case api::RentalFormFactorEnum::CARGO_BICYCLE:\n      return vehicle_form_factor::kCargoBicycle;\n    case api::RentalFormFactorEnum::CAR: return vehicle_form_factor::kCar;\n    case api::RentalFormFactorEnum::MOPED: return vehicle_form_factor::kMoped;\n    case api::RentalFormFactorEnum::SCOOTER_STANDING:\n      return vehicle_form_factor::kScooterStanding;\n    case api::RentalFormFactorEnum::SCOOTER_SEATED:\n      return vehicle_form_factor::kScooterSeated;\n    case api::RentalFormFactorEnum::OTHER: return vehicle_form_factor::kOther;\n  }\n  throw utl::fail(\"invalid rental form factor\");\n}\n\napi::RentalPropulsionTypeEnum to_api_propulsion_type(propulsion_type const pt) {\n  switch (pt) {\n    case propulsion_type::kHuman: return api::RentalPropulsionTypeEnum::HUMAN;\n    case propulsion_type::kElectricAssist:\n      return api::RentalPropulsionTypeEnum::ELECTRIC_ASSIST;\n    case propulsion_type::kElectric:\n      return api::RentalPropulsionTypeEnum::ELECTRIC;\n    case propulsion_type::kCombustion:\n      return api::RentalPropulsionTypeEnum::COMBUSTION;\n    case propulsion_type::kCombustionDiesel:\n      return api::RentalPropulsionTypeEnum::COMBUSTION_DIESEL;\n    case propulsion_type::kHybrid: return api::RentalPropulsionTypeEnum::HYBRID;\n    case propulsion_type::kPlugInHybrid:\n      return api::RentalPropulsionTypeEnum::PLUG_IN_HYBRID;\n    case propulsion_type::kHydrogenFuelCell:\n      return api::RentalPropulsionTypeEnum::HYDROGEN_FUEL_CELL;\n  }\n  std::unreachable();\n}\n\npropulsion_type from_api_propulsion_type(\n    api::RentalPropulsionTypeEnum const pt) {\n  switch (pt) {\n    case api::RentalPropulsionTypeEnum::HUMAN: return propulsion_type::kHuman;\n    case api::RentalPropulsionTypeEnum::ELECTRIC_ASSIST:\n      return propulsion_type::kElectricAssist;\n    case api::RentalPropulsionTypeEnum::ELECTRIC:\n      return propulsion_type::kElectric;\n    case api::RentalPropulsionTypeEnum::COMBUSTION:\n      return propulsion_type::kCombustion;\n    case api::RentalPropulsionTypeEnum::COMBUSTION_DIESEL:\n      return propulsion_type::kCombustionDiesel;\n    case api::RentalPropulsionTypeEnum::HYBRID: return propulsion_type::kHybrid;\n    case api::RentalPropulsionTypeEnum::PLUG_IN_HYBRID:\n      return propulsion_type::kPlugInHybrid;\n    case api::RentalPropulsionTypeEnum::HYDROGEN_FUEL_CELL:\n      return propulsion_type::kHydrogenFuelCell;\n  }\n  throw utl::fail(\"invalid rental propulsion type\");\n}\n\napi::RentalReturnConstraintEnum to_api_return_constraint(\n    return_constraint const rc) {\n  switch (rc) {\n    case return_constraint::kFreeFloating:\n      return api::RentalReturnConstraintEnum::NONE;\n    case return_constraint::kAnyStation:\n      return api::RentalReturnConstraintEnum::ANY_STATION;\n    case return_constraint::kRoundtripStation:\n      return api::RentalReturnConstraintEnum::ROUNDTRIP_STATION;\n  }\n  std::unreachable();\n}\n\nbool products_match(\n    provider_products const& prod,\n    std::optional<std::vector<api::RentalFormFactorEnum>> const& form_factors,\n    std::optional<std::vector<api::RentalPropulsionTypeEnum>> const&\n        propulsion_types) {\n  if (form_factors.has_value() &&\n      utl::find(*form_factors, to_api_form_factor(prod.form_factor_)) ==\n          end(*form_factors)) {\n    return false;\n  }\n  if (propulsion_types.has_value() &&\n      utl::find(*propulsion_types,\n                to_api_propulsion_type(prod.propulsion_type_)) ==\n          end(*propulsion_types)) {\n    return false;\n  }\n  return true;\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/osr_mapping.cc",
    "content": "#include \"motis/gbfs/osr_mapping.h\"\n\n#include <optional>\n#include <utility>\n#include <vector>\n\n#include \"tg.h\"\n\n#include \"geo/box.h\"\n\n#include \"osr/lookup.h\"\n#include \"osr/routing/profiles/foot.h\"\n#include \"osr/types.h\"\n#include \"osr/ways.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/zip.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/types.h\"\n\n#include \"motis/box_rtree.h\"\n#include \"motis/gbfs/compression.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/gbfs/geofencing.h\"\n\nnamespace motis::gbfs {\n\nstruct node_match {\n  osr::way_candidate const& way() const { return wc_; }\n  osr::node_candidate const& node() const {\n    return left_ ? wc_.left_ : wc_.right_;\n  }\n\n  osr::way_candidate wc_;\n  bool left_{};\n};\n\nstruct osr_mapping {\n  osr_mapping(osr::ways const& w,\n              osr::lookup const& l,\n              gbfs_provider const& provider)\n      : w_{w}, l_{l}, provider_{provider} {\n    products_data_.resize(provider.products_.size());\n  }\n\n  void map_geofencing_zones() {\n    auto const make_loc_bitvec = [&]() {\n      auto bv = osr::bitvec<osr::node_idx_t>{};\n      bv.resize(static_cast<typename osr::bitvec<osr::node_idx_t>::size_type>(\n          w_.n_nodes() + provider_.stations_.size() +\n          provider_.vehicle_status_.size()));\n      return bv;\n    };\n\n    auto zone_rtree = box_rtree<std::size_t>{};\n    for (auto const [i, z] :\n         utl::enumerate(provider_.geofencing_zones_.zones_)) {\n      zone_rtree.add(z.bounding_box(), i);\n    }\n\n    for (auto [prod, rd] : utl::zip(provider_.products_, products_data_)) {\n      auto default_restrictions = provider_.default_restrictions_;\n      rd.start_allowed_ = make_loc_bitvec();\n      rd.end_allowed_ = make_loc_bitvec();\n      rd.through_allowed_ = make_loc_bitvec();\n\n      // global rules\n      for (auto const& r : provider_.geofencing_zones_.global_rules_) {\n        if (!applies(r.vehicle_type_idxs_, prod.vehicle_types_)) {\n          continue;\n        }\n        default_restrictions.ride_start_allowed_ = r.ride_start_allowed_;\n        default_restrictions.ride_end_allowed_ = r.ride_end_allowed_;\n        default_restrictions.ride_through_allowed_ = r.ride_through_allowed_;\n        default_restrictions.station_parking_ = r.station_parking_;\n        break;\n      }\n\n      if ((prod.return_constraint_ == return_constraint::kAnyStation ||\n           prod.return_constraint_ == return_constraint::kRoundtripStation) &&\n          (prod.known_return_constraint_ ||\n           provider_.geofencing_zones_.zones_.empty()) &&\n          !default_restrictions.station_parking_.has_value()) {\n        default_restrictions.station_parking_ = true;\n      }\n\n      if (default_restrictions.ride_end_allowed_ &&\n          !default_restrictions.station_parking_.value_or(false)) {\n        rd.end_allowed_.one_out();\n      }\n      if (default_restrictions.ride_through_allowed_) {\n        rd.through_allowed_.one_out();\n      }\n\n      rd.station_parking_ =\n          default_restrictions.station_parking_.value_or(false);\n    }\n\n    auto done = make_loc_bitvec();\n\n    auto zone_indices = std::vector<std::size_t>{};\n    zone_indices.reserve(provider_.geofencing_zones_.zones_.size());\n    auto const handle_point = [&](osr::node_idx_t const n,\n                                  geo::latlng const& pos) {\n      for (auto [prod, rd] : utl::zip(provider_.products_, products_data_)) {\n        auto start_allowed = std::optional<bool>{};\n        auto end_allowed = std::optional<bool>{};\n        auto through_allowed = std::optional<bool>{};\n        auto station_parking = rd.station_parking_;\n\n        // zones have to be checked in the order they are defined\n        zone_indices.clear();\n        zone_rtree.find(pos, [&](std::size_t const zone_idx) {\n          zone_indices.push_back(zone_idx);\n        });\n        utl::sort(zone_indices);\n\n        for (auto const zone_idx : zone_indices) {\n          auto const& z = provider_.geofencing_zones_.zones_[zone_idx];\n          // check if pos is inside the zone multipolygon\n          if (multipoly_contains_point(z.geom_.get(), pos)) {\n            for (auto const& r : z.rules_) {\n              if (!applies(r.vehicle_type_idxs_, prod.vehicle_types_)) {\n                continue;\n              }\n              if (r.station_parking_.has_value()) {\n                station_parking = r.station_parking_.value();\n              }\n              start_allowed = r.ride_start_allowed_;\n              end_allowed = r.ride_end_allowed_ && !station_parking;\n              through_allowed = r.ride_through_allowed_;\n              break;\n            }\n            if (start_allowed.has_value()) {\n              break;  // for now\n            }\n          }\n        }\n        if (end_allowed.has_value()) {\n          rd.end_allowed_.set(n, *end_allowed);\n        }\n        if (through_allowed.has_value()) {\n          rd.through_allowed_.set(n, *through_allowed);\n        }\n      }\n    };\n\n    auto const* osr_r = w_.r_.get();\n    for (auto const& z : provider_.geofencing_zones_.zones_) {\n      l_.find(z.bounding_box(), [&](osr::way_idx_t const way) {\n        for (auto const n : osr_r->way_nodes_[way]) {\n          if (done.test(n)) {\n            continue;\n          }\n          done.set(n, true);\n          handle_point(n, w_.get_node_pos(n).as_latlng());\n        }\n      });\n    }\n  }\n\n  std::vector<node_match> get_node_matches(osr::location const& loc) const {\n    using footp = osr::bike_sharing::footp;\n    using bikep = osr::bike_sharing::bikep;\n    static constexpr auto foot_params = osr::bike_sharing::footp::parameters{};\n    static constexpr auto bike_params = osr::bike_sharing::bikep::parameters{};\n\n    auto is_acceptable_node = [&](osr::node_candidate const& n) {\n      if (!n.valid() || n.dist_to_node_ > kMaxGbfsMatchingDistance) {\n        return false;\n      }\n      auto const& node_props = w_.r_->node_properties_[n.node_];\n      if (footp::node_cost(foot_params, node_props) == osr::kInfeasible ||\n          bikep::node_cost(bike_params, node_props) == osr::kInfeasible) {\n        return false;\n      }\n      // node needs to have at least one way accessible by foot and one by bike\n      return utl::any_of(w_.r_->node_ways_[n.node_],\n                         [&](auto const way_idx) {\n                           return footp::way_cost(\n                                      footp::parameters{},\n                                      w_.r_->way_properties_[way_idx],\n                                      osr::direction::kForward,\n                                      0U) != osr::kInfeasible;\n                         }) &&\n             utl::any_of(w_.r_->node_ways_[n.node_], [&](auto const way_idx) {\n               return bikep::way_cost(\n                          bikep::parameters{}, w_.r_->way_properties_[way_idx],\n                          osr::direction::kForward, 0U) != osr::kInfeasible;\n             });\n    };\n\n    auto const matches = l_.match<footp>(footp::parameters{}, loc, false,\n                                         osr::direction::kForward,\n                                         kMaxGbfsMatchingDistance, nullptr);\n    auto node_matches = std::vector<node_match>{};\n    for (auto const& m : matches) {\n      if (is_acceptable_node(m.left_)) {\n        node_matches.emplace_back(node_match{m, true});\n      }\n      if (is_acceptable_node(m.right_)) {\n        node_matches.emplace_back(node_match{m, false});\n      }\n    }\n    utl::sort(node_matches, [](auto const& a, auto const& b) {\n      return a.node().dist_to_node_ < b.node().dist_to_node_;\n    });\n\n    auto connected_components = hash_set<osr::component_idx_t>{};\n    for (auto it = node_matches.begin(); it != node_matches.end();) {\n      auto const component = w_.r_->way_component_[it->way().way_];\n      if (!connected_components.insert(component).second) {\n        it = node_matches.erase(it);\n      } else {\n        ++it;\n      }\n    }\n\n    return node_matches;\n  }\n\n  void map_stations() {\n    for (auto [prod_b, rd_b] : utl::zip(provider_.products_, products_data_)) {\n      auto& prod = prod_b;  // fix for apple clang\n      auto& rd = rd_b;\n      for (auto const& [id, st] : provider_.stations_) {\n        auto is_renting =\n            st.status_.is_renting_ && st.status_.num_vehicles_available_ > 0;\n        auto is_returning = st.status_.is_returning_;\n\n        // if the station lists vehicles available by type, at least one of\n        // the vehicle types included in the product segment must be available\n        if (is_renting && !st.status_.vehicle_types_available_.empty()) {\n          is_renting = utl::any_of(\n              st.status_.vehicle_types_available_, [&](auto const& vt) {\n                return vt.second != 0 && prod.includes_vehicle_type(vt.first);\n              });\n        }\n\n        // same for returning vehicles\n        if (is_returning && !st.status_.vehicle_docks_available_.empty()) {\n          is_returning = utl::any_of(\n              st.status_.vehicle_docks_available_, [&](auto const& vt) {\n                return vt.second != 0 && prod.includes_vehicle_type(vt.first);\n              });\n        }\n\n        if (!is_renting && !is_returning) {\n          continue;\n        }\n\n        auto const matches =\n            get_node_matches(osr::location{st.info_.pos_, osr::level_t{}});\n        if (matches.empty()) {\n          continue;\n        }\n\n        auto const additional_node_id = add_node(\n            rd, additional_node{additional_node::station{id}}, st.info_.pos_);\n        if (is_renting) {\n          rd.start_allowed_.set(additional_node_id, true);\n        }\n        if (is_returning) {\n          rd.end_allowed_.set(additional_node_id, true);\n          if (st.info_.station_area_) {\n            auto const* geom = st.info_.station_area_.get();\n            auto const rect = tg_geom_rect(geom);\n            auto const bb = geo::box{geo::latlng{rect.min.y, rect.min.x},\n                                     geo::latlng{rect.max.y, rect.max.x}};\n            auto const* osr_r = w_.r_.get();\n            l_.find(bb, [&](osr::way_idx_t const way) {\n              for (auto const n : osr_r->way_nodes_[way]) {\n                if (multipoly_contains_point(geom,\n                                             w_.get_node_pos(n).as_latlng())) {\n                  rd.end_allowed_.set(n, true);\n                }\n              }\n            });\n          }\n        }\n        for (auto const& m : matches) {\n          auto const& node = m.node();\n          auto const edge_to_an = osr::additional_edge{\n              additional_node_id,\n              static_cast<osr::distance_t>(node.dist_to_node_)};\n          auto& node_edges = rd.additional_edges_[node.node_];\n          if (utl::find(node_edges, edge_to_an) == end(node_edges)) {\n            node_edges.emplace_back(edge_to_an);\n          }\n\n          auto const edge_from_an = osr::additional_edge{\n              node.node_, static_cast<osr::distance_t>(node.dist_to_node_)};\n          auto& an_edges = rd.additional_edges_[additional_node_id];\n          if (utl::find(an_edges, edge_from_an) == end(an_edges)) {\n            an_edges.emplace_back(edge_from_an);\n          }\n        }\n      }\n    }\n  }\n\n  void map_vehicles() {\n    for (auto [prod, rd] : utl::zip(provider_.products_, products_data_)) {\n      for (auto const [vehicle_idx, vs] :\n           utl::enumerate(provider_.vehicle_status_)) {\n        if (vs.is_disabled_ || vs.is_reserved_ ||\n            !prod.includes_vehicle_type(vs.vehicle_type_idx_)) {\n          continue;\n        }\n\n        auto const restrictions = provider_.geofencing_zones_.get_restrictions(\n            vs.pos_, vs.vehicle_type_idx_, geofencing_restrictions{});\n        if (!restrictions.ride_start_allowed_) {\n          continue;\n        }\n\n        auto const matches =\n            get_node_matches(osr::location{vs.pos_, osr::level_t{}});\n        if (matches.empty()) {\n          continue;\n        }\n\n        auto const additional_node_id =\n            add_node(rd, additional_node{additional_node::vehicle{vehicle_idx}},\n                     vs.pos_);\n        rd.start_allowed_.set(additional_node_id, true);\n\n        for (auto const& m : matches) {\n          auto const& nc = m.node();\n          auto const edge_to_an = osr::additional_edge{\n              additional_node_id,\n              static_cast<osr::distance_t>(nc.dist_to_node_)};\n          auto& node_edges = rd.additional_edges_[nc.node_];\n          if (utl::find(node_edges, edge_to_an) == end(node_edges)) {\n            node_edges.push_back(edge_to_an);\n          }\n\n          auto const edge_from_an = osr::additional_edge{\n              nc.node_, static_cast<osr::distance_t>(nc.dist_to_node_)};\n          auto& an_edges = rd.additional_edges_[additional_node_id];\n          if (utl::find(an_edges, edge_from_an) == end(an_edges)) {\n            an_edges.push_back(edge_from_an);\n          }\n        }\n      }\n    }\n  }\n\n  osr::node_idx_t add_node(routing_data& rd,\n                           additional_node&& an,\n                           geo::latlng const& pos) const {\n    auto const node_id = static_cast<osr::node_idx_t>(\n        w_.n_nodes() + rd.additional_nodes_.size());\n    rd.additional_nodes_.push_back(std::move(an));\n    rd.additional_node_coordinates_.push_back(pos);\n    assert(rd.start_allowed_.size() >=\n           static_cast<typename osr::bitvec<osr::node_idx_t>::size_type>(\n               node_id + 1));\n    return node_id;\n  }\n\n  osr::ways const& w_;\n  osr::lookup const& l_;\n  gbfs_provider const& provider_;\n\n  std::vector<routing_data> products_data_;\n};\n\nvoid map_data(osr::ways const& w,\n              osr::lookup const& l,\n              gbfs_provider const& provider,\n              provider_routing_data& prd) {\n  auto mapping = osr_mapping{w, l, provider};\n  mapping.map_geofencing_zones();\n  mapping.map_stations();\n  mapping.map_vehicles();\n\n  prd.products_ = utl::to_vec(mapping.products_data_, [&](routing_data& rd) {\n    return compressed_routing_data{\n        .additional_nodes_ = std::move(rd.additional_nodes_),\n        .additional_node_coordinates_ =\n            std::move(rd.additional_node_coordinates_),\n        .additional_edges_ = std::move(rd.additional_edges_),\n        .start_allowed_ = compress_bitvec(rd.start_allowed_),\n        .end_allowed_ = compress_bitvec(rd.end_allowed_),\n        .through_allowed_ = compress_bitvec(rd.through_allowed_)};\n  });\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/osr_profile.cc",
    "content": "#include \"motis/gbfs/osr_profile.h\"\n\nnamespace motis::gbfs {\n\nosr::search_profile get_osr_profile(vehicle_form_factor const& ff) {\n  return ff == vehicle_form_factor::kCar ? osr::search_profile::kCarSharing\n                                         : osr::search_profile::kBikeSharing;\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/parser.cc",
    "content": "#include <optional>\n#include <string_view>\n#include <vector>\n\n#include \"motis/gbfs/parser.h\"\n\n#include \"cista/hash.h\"\n\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/raii.h\"\n#include \"utl/to_vec.h\"\n\nnamespace json = boost::json;\n\nnamespace motis::gbfs {\n\ngbfs_version get_version(json::value const& root) {\n  auto const& root_obj = root.as_object();\n  if (!root_obj.contains(\"version\")) {\n    // 1.0 doesn't have the version key\n    return gbfs_version::k1;\n  }\n  auto const version =\n      static_cast<std::string_view>(root.at(\"version\").as_string());\n  if (version.starts_with(\"1.\")) {\n    return gbfs_version::k1;\n  } else if (version.starts_with(\"2.\")) {\n    return gbfs_version::k2;\n  } else if (version.starts_with(\"3.\")) {\n    return gbfs_version::k3;\n  } else {\n    throw utl::fail(\"unsupported GBFS version: {}\", version);\n  }\n}\n\nstd::string get_localized_string(json::value const& v) {\n  if (v.is_array()) {\n    auto const& arr = v.as_array();\n    if (!arr.empty()) {\n      return static_cast<std::string>(\n          arr[0].as_object().at(\"text\").as_string());\n    }\n    return \"\";\n  } else if (v.is_string()) {\n    return static_cast<std::string>(v.as_string());\n  } else {\n    return \"\";\n  }\n}\n\nstd::string get_as_string(json::object const& obj, std::string_view const key) {\n  auto const val = obj.at(key);\n  if (val.is_string()) {\n    return static_cast<std::string>(val.as_string());\n  } else if (val.is_int64()) {\n    return std::to_string(val.as_int64());\n  } else if (val.is_uint64()) {\n    return std::to_string(val.as_uint64());\n  } else {\n    return json::serialize(val);\n  }\n}\n\nstd::string optional_str(json::object const& obj, std::string_view key) {\n  return obj.contains(key) ? get_as_string(obj, key) : \"\";\n}\n\nstd::string optional_localized_str(json::object const& obj,\n                                   std::string_view key) {\n  return obj.contains(key) ? get_localized_string(obj.at(key)) : \"\";\n}\n\nbool get_bool(json::object const& obj,\n              std::string_view const key,\n              std::optional<bool> const def = std::nullopt) {\n  if (!obj.contains(key) && def.has_value()) {\n    return *def;\n  }\n  auto const val = obj.at(key);\n  if (val.is_bool()) {\n    return val.as_bool();\n  } else if (val.is_number()) {\n    return val.to_number<int>() == 1;\n  } else {\n    return *def;\n  }\n}\n\ntg_geom* parse_multipolygon(json::object const& json) {\n  utl::verify(json.at(\"type\").as_string() == \"MultiPolygon\",\n              \"expected MultiPolygon, got {}\", json.at(\"type\").as_string());\n  auto const& coordinates = json.at(\"coordinates\").as_array();\n\n  auto polys = std::vector<tg_poly*>{};\n  UTL_FINALLY([&polys]() {\n    for (auto const poly : polys) {\n      tg_poly_free(poly);\n    }\n  })\n\n  for (auto const& j_poly : coordinates) {\n    auto rings = std::vector<tg_ring*>{};\n    UTL_FINALLY([&rings]() {\n      for (auto const ring : rings) {\n        tg_ring_free(ring);\n      }\n    })\n    for (auto const& j_ring : j_poly.as_array()) {\n      auto points = utl::to_vec(j_ring.as_array(), [&](auto const& j_pt) {\n        auto const& j_pt_arr = j_pt.as_array();\n        utl::verify(j_pt_arr.size() >= 2, \"invalid point in polygon ring\");\n        return tg_point{j_pt_arr[0].as_double(), j_pt_arr[1].as_double()};\n      });\n      utl::verify(points.size() > 2, \"empty ring in polygon\");\n      // handle invalid polygons that don't have closed rings\n      if (points.front().x != points.back().x ||\n          points.front().y != points.back().y) {\n        points.push_back(points.front());\n      }\n      auto ring = tg_ring_new(points.data(), static_cast<int>(points.size()));\n      utl::verify(ring != nullptr, \"failed to create ring\");\n      rings.emplace_back(ring);\n    }\n    utl::verify(!rings.empty(), \"empty polygon in multipolygon\");\n    auto poly =\n        tg_poly_new(rings.front(), rings.size() > 1 ? &rings[1] : nullptr,\n                    static_cast<int>(rings.size() - 1));\n    utl::verify(poly != nullptr, \"failed to create polygon\");\n    polys.emplace_back(poly);\n  }\n\n  utl::verify(!polys.empty(), \"empty multipolygon\");\n  auto const multipoly =\n      tg_geom_new_multipolygon(polys.data(), static_cast<int>(polys.size()));\n  utl::verify(multipoly != nullptr, \"failed to create multipolygon\");\n  return multipoly;\n}\n\nhash_map<std::string, std::string> parse_discovery(json::value const& root) {\n  auto urls = hash_map<std::string, std::string>{};\n\n  auto const& data = root.at(\"data\").as_object();\n  if (data.empty()) {\n    return urls;\n  }\n  auto const& feeds =\n      data.contains(\"feeds\")\n          ? data.at(\"feeds\").as_array()\n          : data.begin()->value().as_object().at(\"feeds\").as_array();\n\n  for (auto const& feed : feeds) {\n    auto const& name =\n        static_cast<std::string>(feed.as_object().at(\"name\").as_string());\n    auto const& url =\n        static_cast<std::string>(feed.as_object().at(\"url\").as_string());\n    urls[name] = url;\n  }\n  return urls;\n}\n\nrental_uris parse_rental_uris(json::object const& parent) {\n  auto uris = rental_uris{};\n\n  if (parent.contains(\"rental_uris\")) {\n    auto const& o = parent.at(\"rental_uris\").as_object();\n    uris.android_ = optional_str(o, \"android\");\n    uris.ios_ = optional_str(o, \"ios\");\n    uris.web_ = optional_str(o, \"web\");\n  }\n\n  return uris;\n}\n\nstd::optional<vehicle_type_idx_t> get_vehicle_type(\n    gbfs_provider& provider,\n    std::string const& vehicle_type_id,\n    vehicle_start_type const start_type) {\n  auto const add_vehicle_type = [&](vehicle_form_factor const ff,\n                                    propulsion_type const pt,\n                                    std::string const& name) {\n    auto const idx = vehicle_type_idx_t{provider.vehicle_types_.size()};\n    provider.vehicle_types_.emplace_back(vehicle_type{\n        .id_ = vehicle_type_id,\n        .idx_ = idx,\n        .name_ = name,\n        .form_factor_ = ff,\n        .propulsion_type_ = pt,\n        .return_constraint_ = provider.default_return_constraint_.value_or(\n            start_type == vehicle_start_type::kStation\n                ? return_constraint::kAnyStation\n                : return_constraint::kFreeFloating),\n        .known_return_constraint_ = false});\n    provider.vehicle_types_map_[{vehicle_type_id, start_type}] = idx;\n    return idx;\n  };\n\n  if (auto const it =\n          provider.vehicle_types_map_.find({vehicle_type_id, start_type});\n      it != end(provider.vehicle_types_map_)) {\n    return it->second;\n  } else if (auto const temp_it =\n                 provider.temp_vehicle_types_.find(vehicle_type_id);\n             temp_it != end(provider.temp_vehicle_types_)) {\n    return add_vehicle_type(temp_it->second.form_factor_,\n                            temp_it->second.propulsion_type_,\n                            temp_it->second.name_);\n  } else if (vehicle_type_id.empty()) {\n    // providers that don't use vehicle types\n    return add_vehicle_type(vehicle_form_factor::kBicycle,\n                            propulsion_type::kHuman, \"\");\n  }\n  return {};\n}\n\nvoid load_system_information(gbfs_provider& provider, json::value const& root) {\n  auto const& data = root.at(\"data\").as_object();\n\n  auto& si = provider.sys_info_;\n  si.id_ = static_cast<std::string>(data.at(\"system_id\").as_string());\n  si.name_ = get_localized_string(data.at(\"name\"));\n  si.name_short_ = optional_localized_str(data, \"name_short\");\n  si.operator_ = optional_localized_str(data, \"operator\");\n  si.url_ = optional_str(data, \"url\");\n  si.purchase_url_ = optional_str(data, \"purchase_url\");\n  si.mail_ = optional_str(data, \"email\");\n  if (data.contains(\"brand_assets\")) {\n    auto const& ba = data.at(\"brand_assets\").as_object();\n    si.color_ = optional_str(ba, \"color\");\n  } else {\n    si.color_ = \"\";\n  }\n}\n\nvoid load_station_information(gbfs_provider& provider,\n                              json::value const& root) {\n  provider.stations_.clear();\n\n  auto const& stations_arr = root.at(\"data\").at(\"stations\").as_array();\n  for (auto const& s : stations_arr) {\n    auto const& station_obj = s.as_object();\n    auto const station_id = get_as_string(station_obj, \"station_id\");\n    try {\n      auto const name = get_localized_string(station_obj.at(\"name\"));\n      auto const lat = station_obj.at(\"lat\").as_double();\n      auto const lon = station_obj.at(\"lon\").as_double();\n\n      tg_geom* area = nullptr;\n      if (station_obj.contains(\"station_area\")) {\n        try {\n          area = parse_multipolygon(station_obj.at(\"station_area\").as_object());\n        } catch (std::exception const& ex) {\n          std::cerr << \"[GBFS] (\" << provider.id_\n                    << \") invalid station_area: \" << ex.what() << \"\\n\";\n        }\n      }\n\n      provider.stations_[station_id] = station{\n          .info_ = {.id_ = station_id,\n                    .name_ = name,\n                    .pos_ = geo::latlng{lat, lon},\n                    .address_ = optional_str(station_obj, \"address\"),\n                    .cross_street_ = optional_str(station_obj, \"cross_street\"),\n                    .rental_uris_ = parse_rental_uris(station_obj),\n                    .station_area_ =\n                        std::shared_ptr<tg_geom>(area, tg_geom_deleter{})}};\n    } catch (std::exception const& ex) {\n      std::cerr << \"[GBFS] (\" << provider.id_ << \") error parsing station \"\n                << station_id << \": \" << ex.what() << \"\\n\";\n    }\n  }\n}\n\nvoid load_station_status(gbfs_provider& provider, json::value const& root) {\n  auto const& stations_arr = root.at(\"data\").at(\"stations\").as_array();\n  for (auto const& s : stations_arr) {\n    auto const& station_obj = s.as_object();\n    auto const station_id = get_as_string(station_obj, \"station_id\");\n\n    auto const station_it = provider.stations_.find(station_id);\n    if (station_it == end(provider.stations_)) {\n      continue;\n    }\n\n    auto& station = station_it->second;\n    station.status_ = station_status{\n        .num_vehicles_available_ = 0U,\n        .is_renting_ = get_bool(station_obj, \"is_renting\", true),\n        .is_returning_ = get_bool(station_obj, \"is_returning\", true)};\n\n    if (station_obj.contains(\"num_vehicles_available\")) {\n      // GBFS 3.x (but some 2.x feeds use this as well)\n      station.status_.num_vehicles_available_ =\n          station_obj.at(\"num_vehicles_available\").to_number<unsigned>();\n    } else if (station_obj.contains(\"num_bikes_available\")) {\n      // GBFS 2.x\n      station.status_.num_vehicles_available_ =\n          station_obj.at(\"num_bikes_available\").to_number<unsigned>();\n    }\n\n    if (station_obj.contains(\"vehicle_types_available\")) {\n      auto const& vta = station_obj.at(\"vehicle_types_available\").as_array();\n      auto unrestricted_available = 0U;\n      auto any_station_available = 0U;\n      auto roundtrip_available = 0U;\n      for (auto const& vt : vta) {\n        auto const vehicle_type_id =\n            static_cast<std::string>(vt.at(\"vehicle_type_id\").as_string());\n        auto const count = vt.at(\"count\").to_number<unsigned>();\n        if (auto const vt_idx = get_vehicle_type(provider, vehicle_type_id,\n                                                 vehicle_start_type::kStation);\n            vt_idx) {\n          station.status_.vehicle_types_available_[*vt_idx] = count;\n          switch (provider.vehicle_types_[*vt_idx].return_constraint_) {\n            case return_constraint::kFreeFloating:\n              unrestricted_available += count;\n              break;\n            case return_constraint::kAnyStation:\n              any_station_available += count;\n              break;\n            case return_constraint::kRoundtripStation:\n              roundtrip_available += count;\n              break;\n          }\n        }\n      }\n      station.status_.num_vehicles_available_ =\n          unrestricted_available + any_station_available + roundtrip_available;\n    } else {\n      if (auto const vt_idx =\n              get_vehicle_type(provider, \"\", vehicle_start_type::kStation);\n          vt_idx) {\n        station.status_.vehicle_types_available_[*vt_idx] =\n            station.status_.num_vehicles_available_;\n      }\n    }\n\n    if (station_obj.contains(\"vehicle_docks_available\")) {\n      for (auto const& vt :\n           station_obj.at(\"vehicle_docks_available\").as_array()) {\n        auto& vto = vt.as_object();\n        if (vto.contains(\"vehicle_type_ids\") && vto.contains(\"count\")) {\n          for (auto const& vti : vto.at(\"vehicle_type_ids\").as_array()) {\n            auto const vehicle_type_id =\n                static_cast<std::string>(vti.as_string());\n            if (auto const vt_idx = get_vehicle_type(\n                    provider, vehicle_type_id, vehicle_start_type::kStation);\n                vt_idx) {\n              station.status_.vehicle_docks_available_[*vt_idx] =\n                  vto.at(\"count\").to_number<unsigned>();\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\nvehicle_form_factor parse_form_factor(std::string_view const s) {\n  switch (cista::hash(s)) {\n    case cista::hash(\"bicycle\"):\n    case cista::hash(\"bike\"):  // non-standard\n      return vehicle_form_factor::kBicycle;\n    case cista::hash(\"cargo_bicycle\"):\n      return vehicle_form_factor::kCargoBicycle;\n    case cista::hash(\"car\"): return vehicle_form_factor::kCar;\n    case cista::hash(\"moped\"): return vehicle_form_factor::kMoped;\n    case cista::hash(\"scooter\"):  // < 3.0\n    case cista::hash(\"scooter_standing\"):\n      return vehicle_form_factor::kScooterStanding;\n    case cista::hash(\"scooter_seated\"):\n      return vehicle_form_factor::kScooterSeated;\n    case cista::hash(\"other\"):\n    default: return vehicle_form_factor::kOther;\n  }\n}\n\npropulsion_type parse_propulsion_type(std::string_view const s) {\n  switch (cista::hash(s)) {\n    case cista::hash(\"human\"): return propulsion_type::kHuman;\n    case cista::hash(\"electric_assist\"):\n      return propulsion_type::kElectricAssist;\n    case cista::hash(\"electric\"): return propulsion_type::kElectric;\n    case cista::hash(\"combustion\"): return propulsion_type::kCombustion;\n    case cista::hash(\"combustion_diesel\"):\n      return propulsion_type::kCombustionDiesel;\n    case cista::hash(\"hybrid\"): return propulsion_type::kHybrid;\n    case cista::hash(\"plug_in_hybrid\"): return propulsion_type::kPlugInHybrid;\n    case cista::hash(\"hydrogen_fuel_cell\"):\n      return propulsion_type::kHydrogenFuelCell;\n    default: return propulsion_type::kHuman;\n  }\n}\n\nstd::optional<return_constraint> parse_return_constraint(\n    std::string_view const s) {\n  switch (cista::hash(s)) {\n    case cista::hash(\"any_station\"): return return_constraint::kAnyStation;\n    case cista::hash(\"roundtrip_station\"):\n      return return_constraint::kRoundtripStation;\n    case cista::hash(\"free_floating\"):\n    case cista::hash(\"hybrid\"): return return_constraint::kFreeFloating;\n    default: return {};\n  }\n}\n\nstd::optional<return_constraint> parse_return_constraint(\n    json::object const& vt) {\n  if (vt.contains(\"return_constraint\")) {\n    return parse_return_constraint(vt.at(\"return_constraint\").as_string());\n  }\n  return {};\n}\n\nvoid load_vehicle_types(gbfs_provider& provider, json::value const& root) {\n  provider.vehicle_types_.clear();\n  provider.vehicle_types_map_.clear();\n  provider.temp_vehicle_types_.clear();\n  for (auto const& v : root.at(\"data\").at(\"vehicle_types\").as_array()) {\n    auto const id =\n        static_cast<std::string>(v.at(\"vehicle_type_id\").as_string());\n    auto const name = optional_localized_str(v.as_object(), \"name\");\n    auto const rc = parse_return_constraint(v.as_object());\n    auto const form_factor =\n        parse_form_factor(optional_str(v.as_object(), \"form_factor\"));\n    auto const propulsion_type =\n        parse_propulsion_type(optional_str(v.as_object(), \"propulsion_type\"));\n    if (rc) {\n      auto const idx = vehicle_type_idx_t{provider.vehicle_types_.size()};\n      provider.vehicle_types_.emplace_back(\n          vehicle_type{.id_ = id,\n                       .idx_ = idx,\n                       .name_ = name,\n                       .form_factor_ = form_factor,\n                       .propulsion_type_ = propulsion_type,\n                       .return_constraint_ = *rc,\n                       .known_return_constraint_ = true});\n      provider.vehicle_types_map_[{id, vehicle_start_type::kStation}] = idx;\n      provider.vehicle_types_map_[{id, vehicle_start_type::kFreeFloating}] =\n          idx;\n    } else {\n      provider.temp_vehicle_types_[id] = temp_vehicle_type{\n          .id_ = id,\n          .name_ = name,\n          .form_factor_ = form_factor,\n          .propulsion_type_ = propulsion_type,\n      };\n    }\n  }\n}\n\nvoid load_vehicle_status(gbfs_provider& provider, json::value const& root) {\n  provider.vehicle_status_.clear();\n\n  auto const version = get_version(root);\n  auto const& vehicles_arr =\n      root.at(\"data\")\n          .at(version == gbfs_version::k3 ? \"vehicles\" : \"bikes\")\n          .as_array();\n  for (auto const& v : vehicles_arr) {\n    auto const& vehicle_obj = v.as_object();\n\n    auto pos = geo::latlng{};\n    if (vehicle_obj.contains(\"lat\") && vehicle_obj.contains(\"lon\")) {\n      auto const lat = vehicle_obj.at(\"lat\");\n      auto const lon = vehicle_obj.at(\"lon\");\n      if (!lat.is_double() || !lon.is_double()) {\n        continue;\n      }\n      pos = geo::latlng{vehicle_obj.at(\"lat\").as_double(),\n                        vehicle_obj.at(\"lon\").as_double()};\n    } else if (vehicle_obj.contains(\"station_id\")) {\n      auto const station_id = get_as_string(vehicle_obj, \"station_id\");\n      if (auto const it = provider.stations_.find(station_id);\n          it != end(provider.stations_)) {\n        pos = it->second.info_.pos_;\n      } else {\n        continue;\n      }\n    } else {\n      continue;\n    }\n\n    auto const id = get_as_string(\n        vehicle_obj, version == gbfs_version::k3 ? \"vehicle_id\" : \"bike_id\");\n\n    auto const type_id = optional_str(vehicle_obj, \"vehicle_type_id\");\n    auto type_idx = vehicle_type_idx_t::invalid();\n\n    if (auto const vt_idx = get_vehicle_type(provider, type_id,\n                                             vehicle_start_type::kFreeFloating);\n        vt_idx) {\n      type_idx = *vt_idx;\n    }\n\n    provider.vehicle_status_.emplace_back(vehicle_status{\n        .id_ = id,\n        .pos_ = pos,\n        .is_reserved_ = get_bool(vehicle_obj, \"is_reserved\", false),\n        .is_disabled_ = get_bool(vehicle_obj, \"is_disabled\", false),\n        .vehicle_type_idx_ = type_idx,\n        .station_id_ = optional_str(vehicle_obj, \"station_id\"),\n        .home_station_id_ = optional_str(vehicle_obj, \"home_station_id\"),\n        .rental_uris_ = parse_rental_uris(vehicle_obj)});\n  }\n\n  utl::sort(provider.vehicle_status_);\n}\n\nrule parse_rule(gbfs_provider& provider,\n                gbfs_version const version,\n                json::value const& r) {\n  auto const vti_key =\n      version == gbfs_version::k2 ? \"vehicle_type_id\" : \"vehicle_type_ids\";\n  auto const& rule_obj = r.as_object();\n\n  auto vehicle_type_idxs = std::vector<vehicle_type_idx_t>{};\n  if (rule_obj.contains(vti_key)) {\n    for (auto const& vt : rule_obj.at(vti_key).as_array()) {\n      auto const vt_id = static_cast<std::string>(vt.as_string());\n      if (auto const it = provider.vehicle_types_map_.find(\n              {vt_id, vehicle_start_type::kStation});\n          it != end(provider.vehicle_types_map_)) {\n        vehicle_type_idxs.emplace_back(it->second);\n      }\n      if (auto const it = provider.vehicle_types_map_.find(\n              {vt_id, vehicle_start_type::kFreeFloating});\n          it != end(provider.vehicle_types_map_)) {\n        vehicle_type_idxs.emplace_back(it->second);\n      }\n    }\n  }\n\n  return rule{\n      .vehicle_type_idxs_ = std::move(vehicle_type_idxs),\n      .ride_start_allowed_ = version == gbfs_version::k2\n                                 ? rule_obj.at(\"ride_allowed\").as_bool()\n                                 : rule_obj.at(\"ride_start_allowed\").as_bool(),\n      .ride_end_allowed_ = version == gbfs_version::k2\n                               ? rule_obj.at(\"ride_allowed\").as_bool()\n                               : rule_obj.at(\"ride_end_allowed\").as_bool(),\n      .ride_through_allowed_ = rule_obj.at(\"ride_through_allowed\").as_bool(),\n      .station_parking_ =\n          rule_obj.contains(\"station_parking\")\n              ? std::optional{rule_obj.at(\"station_parking\").as_bool()}\n              : std::nullopt};\n}\n\nvoid load_geofencing_zones(gbfs_provider& provider, json::value const& root) {\n  auto const version = get_version(root);\n\n  auto const& zones_obj = root.at(\"data\").at(\"geofencing_zones\").as_object();\n  utl::verify(zones_obj.at(\"type\") == \"FeatureCollection\",\n              \"invalid geofencing_zones\");\n\n  auto zones = std::vector<zone>{};\n  auto const zones_arr = zones_obj.at(\"features\").as_array();\n  zones.reserve(zones_arr.size());\n  for (auto const& z : zones_arr) {\n    try {\n      auto const& props = z.at(\"properties\").as_object();\n      if (!props.contains(\"rules\") || !props.at(\"rules\").is_array()) {\n        continue;\n      }\n      auto rules = utl::to_vec(\n          props.at(\"rules\").as_array(),\n          [&](auto const& r) { return parse_rule(provider, version, r); });\n\n      auto* geom = parse_multipolygon(z.at(\"geometry\").as_object());\n\n      auto name = optional_localized_str(props, \"name\");\n\n      zones.emplace_back(geom, std::move(rules), std::move(name));\n    } catch (std::exception const& ex) {\n      std::cerr << \"[GBFS] (\" << provider.id_\n                << \") invalid geofencing zone: \" << ex.what() << \"\\n\";\n    }\n  }\n\n  //  required in 3.0, but some feeds don't have it\n  auto global_rules =\n      root.at(\"data\").as_object().contains(\"global_rules\") &&\n              root.at(\"data\").at(\"global_rules\").is_array()\n          ? utl::to_vec(\n                root.at(\"data\").at(\"global_rules\").as_array(),\n                [&](auto const& r) { return parse_rule(provider, version, r); })\n          : std::vector<rule>{};\n\n  provider.geofencing_zones_.version_ = version;\n  provider.geofencing_zones_.zones_ = std::move(zones);\n  provider.geofencing_zones_.global_rules_ = std::move(global_rules);\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/routing_data.cc",
    "content": "#include \"motis/gbfs/routing_data.h\"\n\n#include \"osr/lookup.h\"\n#include \"osr/types.h\"\n#include \"osr/ways.h\"\n\n#include \"fmt/format.h\"\n\n#include \"utl/get_or_create.h\"\n#include \"utl/timer.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/gbfs/osr_mapping.h\"\n#include \"motis/transport_mode_ids.h\"\n\nnamespace motis::gbfs {\n\nstd::shared_ptr<provider_routing_data> compute_provider_routing_data(\n    osr::ways const& w, osr::lookup const& l, gbfs_provider const& provider) {\n  auto timer = utl::scoped_timer{\n      fmt::format(\"compute routing data for gbfs provider {}\", provider.id_)};\n  auto prd = std::make_shared<provider_routing_data>();\n\n  map_data(w, l, provider, *prd);\n\n  return prd;\n}\n\nstd::shared_ptr<provider_routing_data> get_provider_routing_data(\n    osr::ways const& w,\n    osr::lookup const& l,\n    gbfs_data& data,\n    gbfs_provider const& provider) {\n  return data.cache_.get_or_compute(provider.idx_, [&]() {\n    return compute_provider_routing_data(w, l, provider);\n  });\n}\n\nstd::shared_ptr<provider_routing_data>\ngbfs_routing_data::get_provider_routing_data(gbfs_provider const& provider) {\n  return gbfs::get_provider_routing_data(*w_, *l_, *data_, provider);\n}\n\nproducts_routing_data* gbfs_routing_data::get_products_routing_data(\n    gbfs_provider const& provider, gbfs_products_idx_t const prod_idx) {\n  auto const ref = gbfs::gbfs_products_ref{provider.idx_, prod_idx};\n  return utl::get_or_create(\n             products_, ref,\n             [&] { return data_->get_products_routing_data(*w_, *l_, ref); })\n      .get();\n}\n\nproducts_routing_data* gbfs_routing_data::get_products_routing_data(\n    gbfs_products_ref const prod_ref) {\n  return get_products_routing_data(*data_->providers_.at(prod_ref.provider_),\n                                   prod_ref.products_);\n}\n\nprovider_products const& gbfs_routing_data::get_products(\n    gbfs_products_ref const prod_ref) {\n  return data_->providers_.at(prod_ref.provider_)\n      ->products_.at(prod_ref.products_);\n}\n\nnigiri::transport_mode_id_t gbfs_routing_data::get_transport_mode(\n    gbfs_products_ref const prod_ref) {\n  return utl::get_or_create(products_ref_to_transport_mode_, prod_ref, [&]() {\n    auto const id = static_cast<nigiri::transport_mode_id_t>(\n        kGbfsTransportModeIdOffset + products_refs_.size());\n    products_refs_.emplace_back(prod_ref);\n    return id;\n  });\n}\n\ngbfs_products_ref gbfs_routing_data::get_products_ref(\n    nigiri::transport_mode_id_t const id) const {\n  return products_refs_.at(\n      static_cast<std::size_t>(id - kGbfsTransportModeIdOffset));\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/gbfs/update.cc",
    "content": "#include \"motis/gbfs/update.h\"\n\n#include <algorithm>\n#include <cassert>\n#include <chrono>\n#include <filesystem>\n#include <fstream>\n#include <set>\n#include <sstream>\n#include <string_view>\n#include <utility>\n\n#include <iostream>\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/asio/experimental/awaitable_operators.hpp\"\n#include \"boost/asio/experimental/parallel_group.hpp\"\n#include \"boost/asio/redirect_error.hpp\"\n#include \"boost/asio/steady_timer.hpp\"\n#include \"boost/stacktrace.hpp\"\n\n#include \"boost/json.hpp\"\n\n#include \"boost/url/encode.hpp\"\n#include \"boost/url/rfc/unreserved_chars.hpp\"\n\n#include \"cista/hash.h\"\n\n#include \"fmt/format.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/overloaded.h\"\n#include \"utl/sorted_diff.h\"\n#include \"utl/timer.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/verify.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/gbfs/data.h\"\n#include \"motis/http_req.h\"\n\n#include \"motis/gbfs/compression.h\"\n#include \"motis/gbfs/osr_mapping.h\"\n#include \"motis/gbfs/parser.h\"\n#include \"motis/gbfs/partition.h\"\n#include \"motis/gbfs/routing_data.h\"\n\nnamespace asio = boost::asio;\nusing asio::awaitable;\nusing namespace asio::experimental::awaitable_operators;\n\nnamespace json = boost::json;\n\nnamespace motis::gbfs {\n\nstruct gbfs_file {\n  json::value json_;\n  cista::hash_t hash_{};\n  std::chrono::system_clock::time_point next_refresh_;\n};\n\nstd::string read_file(std::filesystem::path const& path) {\n  auto is = std::ifstream{path};\n  auto buf = std::stringstream{};\n  buf << is.rdbuf();\n  return buf.str();\n}\n\nbool needs_refresh(file_info const& fi) {\n  return fi.needs_update(std::chrono::system_clock::now());\n}\n\n// try to hash only the value of the \"data\" key to ignore fields like\n// \"last_updated\"\ncista::hash_t hash_gbfs_data(std::string_view const json) {\n  auto const pos = json.find(\"\\\"data\\\"\");\n  if (pos == std::string_view::npos) {\n    return cista::hash(json);\n  }\n\n  auto i = pos + 6;\n  auto const skip_whitespace = [&]() {\n    while (i < json.size() && (json[i] == ' ' || json[i] == '\\n' ||\n                               json[i] == '\\r' || json[i] == '\\t')) {\n      ++i;\n    }\n  };\n  skip_whitespace();\n\n  if (i >= json.size() || json[i++] != ':') {\n    return cista::hash(json);\n  }\n\n  skip_whitespace();\n\n  if (i >= json.size() || json[i] != '{') {\n    return cista::hash(json);\n  }\n\n  auto const start = i;\n  auto depth = 1;\n  auto in_string = false;\n\n  while (++i < json.size()) {\n    if (in_string) {\n      if (json[i] == '\"' && json[i - 1] != '\\\\') {\n        in_string = false;\n      }\n      continue;\n    }\n\n    switch (json[i]) {\n      case '\"': in_string = true; break;\n      case '{': ++depth; break;\n      case '}':\n        if (--depth == 0) {\n          return cista::hash(json.substr(start, i - start + 1));\n        }\n    }\n  }\n\n  return cista::hash(json);\n}\n\nstd::chrono::system_clock::time_point get_expiry(\n    json::object const& root,\n    std::chrono::seconds const def = std::chrono::seconds{0},\n    std::map<std::string, unsigned> const& default_ttl = {},\n    std::map<std::string, unsigned> const& overwrite_ttl = {},\n    std::string_view const name = \"\") {\n  auto const now = std::chrono::system_clock::now();\n  if (auto const it = overwrite_ttl.find(std::string{name});\n      it != end(overwrite_ttl)) {\n    return now + std::chrono::seconds{it->second};\n  }\n  if (root.contains(\"data\")) {\n    auto const& data = root.at(\"data\").as_object();\n    if (data.contains(\"ttl\")) {\n      auto const ttl = data.at(\"ttl\").to_number<int>();\n      if (ttl > 0) {\n        return now + std::chrono::seconds{ttl};\n      }\n    }\n  }\n  if (auto const it = default_ttl.find(std::string{name});\n      it != end(default_ttl)) {\n    return now + std::chrono::seconds{it->second};\n  }\n  return now + def;\n}\n\nstruct gbfs_update {\n  gbfs_update(config::gbfs const& c,\n              osr::ways const& w,\n              osr::lookup const& l,\n              gbfs_data* d,\n              gbfs_data const* prev_d)\n      : c_{c},\n        w_{w},\n        l_{l},\n        d_{d},\n        prev_d_{prev_d},\n        timeout_{c.http_timeout_},\n        proxy_{c.proxy_.transform([](std::string const& u) {\n          auto const url = boost::urls::url{u};\n\n          auto p = proxy{};\n          p.use_tls_ = url.scheme_id() == boost::urls::scheme::https;\n          p.host_ = url.host();\n          p.port_ = url.has_port() ? url.port() : (p.use_tls_ ? \"443\" : \"80\");\n          return p;\n        })} {}\n\n  awaitable<void> run() {\n    auto executor = co_await asio::this_coro::executor;\n    if (prev_d_ == nullptr) {\n      // this is first time gbfs_update is run: initialize feeds from config\n      d_->aggregated_feeds_ =\n          std::make_shared<std::vector<std::unique_ptr<aggregated_feed>>>();\n      d_->standalone_feeds_ =\n          std::make_shared<std::vector<std::unique_ptr<provider_feed>>>();\n\n      for (auto const& [id, group] : c_.groups_) {\n        d_->groups_.emplace(id, gbfs_group{.id_ = id,\n                                           .name_ = group.name_.value_or(id),\n                                           .color_ = group.color_});\n      }\n\n      auto awaitables = utl::to_vec(c_.feeds_, [&](auto const& f) {\n        auto const& id = f.first;\n        auto const& feed = f.second;\n        auto const dir =\n            feed.url_.starts_with(\"http:\") || feed.url_.starts_with(\"https:\")\n                ? std::nullopt\n                : std::optional<std::filesystem::path>{feed.url_};\n\n        return boost::asio::co_spawn(\n            executor,\n            [this, id, feed, dir]() -> awaitable<void> {\n              co_await init_feed(id, feed, dir);\n            },\n            asio::deferred);\n      });\n\n      co_await asio::experimental::make_parallel_group(awaitables)\n          .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable);\n    } else {\n      // update run: copy over data from previous state and update feeds\n      // where necessary\n      d_->aggregated_feeds_ = prev_d_->aggregated_feeds_;\n      d_->standalone_feeds_ = prev_d_->standalone_feeds_;\n      // the set of providers can change if aggregated feeds are used + change.\n      // gbfs_provider_idx_t for existing providers is stable, if a provider is\n      // removed its entry is set to a nullptr. new providers may be added.\n      d_->providers_.resize(prev_d_->providers_.size());\n      d_->provider_by_id_ = prev_d_->provider_by_id_;\n      d_->provider_rtree_ = prev_d_->provider_rtree_;\n      d_->provider_zone_rtree_ = prev_d_->provider_zone_rtree_;\n      d_->cache_ = prev_d_->cache_;\n\n      d_->groups_ = prev_d_->groups_;\n      for (auto& group : d_->groups_ | std::views::values) {\n        group.providers_.clear();\n      }\n\n      co_await refresh_oauth_tokens();\n\n      if (!d_->aggregated_feeds_->empty()) {\n        co_await asio::experimental::make_parallel_group(\n            utl::to_vec(*d_->aggregated_feeds_,\n                        [&](auto const& af) {\n                          return boost::asio::co_spawn(\n                              executor,\n                              [this, af = af.get()]() -> awaitable<void> {\n                                co_await update_aggregated_feed(*af);\n                              },\n                              asio::deferred);\n                        }))\n            .async_wait(asio::experimental::wait_for_all(),\n                        asio::use_awaitable);\n      }\n\n      if (!d_->standalone_feeds_->empty()) {\n        co_await asio::experimental::make_parallel_group(\n            utl::to_vec(*d_->standalone_feeds_,\n                        [&](auto const& pf) {\n                          return boost::asio::co_spawn(\n                              executor,\n                              [this, pf = pf.get()]() -> awaitable<void> {\n                                co_await update_provider_feed(*pf);\n                              },\n                              asio::deferred);\n                        }))\n            .async_wait(asio::experimental::wait_for_all(),\n                        asio::use_awaitable);\n      }\n    }\n  }\n\n  awaitable<void> init_feed(std::string const& id,\n                            config::gbfs::feed const& config,\n                            std::optional<std::filesystem::path> const& dir) {\n    // initialization of a (standalone or aggregated) feed from the config\n    try {\n      auto const headers = config.headers_.value_or(headers_t{});\n      auto oauth = std::shared_ptr<oauth_state>{};\n      if (config.oauth_) {\n        oauth = std::make_shared<oauth_state>(\n            oauth_state{.settings_ = *config.oauth_,\n                        .expires_in_ = config.oauth_->expires_in_.value_or(0)});\n      }\n\n      auto const merge_ttl_map =\n          [](std::optional<std::map<std::string, unsigned>> const& feed_map,\n             std::optional<std::map<std::string, unsigned>> const& global_map) {\n            auto res = global_map.value_or(std::map<std::string, unsigned>{});\n            if (feed_map) {\n              for (auto const& [k, v] : *feed_map) {\n                res[k] = v;\n              }\n            }\n            return res;\n          };\n\n      auto const default_ttl =\n          merge_ttl_map(config.ttl_.value_or(config::gbfs::ttl{}).default_,\n                        c_.ttl_.value_or(config::gbfs::ttl{}).default_);\n      auto const overwrite_ttl =\n          merge_ttl_map(config.ttl_.value_or(config::gbfs::ttl{}).overwrite_,\n                        c_.ttl_.value_or(config::gbfs::ttl{}).overwrite_);\n\n      auto discovery = co_await fetch_file(\"gbfs\", config.url_, headers, oauth,\n                                           dir, default_ttl, overwrite_ttl);\n      auto const& root = discovery.json_.as_object();\n      if ((root.contains(\"data\") &&\n           root.at(\"data\").as_object().contains(\"datasets\")) ||\n          root.contains(\"systems\")) {\n        // file is not an individual feed, but a manifest.json / Lamassu file\n        co_return co_await init_aggregated_feed(id, config.url_, headers,\n                                                std::move(oauth), root,\n                                                default_ttl, overwrite_ttl);\n      }\n\n      auto saf =\n          d_->standalone_feeds_\n              ->emplace_back(std::make_unique<provider_feed>(provider_feed{\n                  .id_ = id,\n                  .url_ = config.url_,\n                  .headers_ = headers,\n                  .dir_ = dir,\n                  .default_restrictions_ = lookup_default_restrictions(\"\", id),\n                  .default_return_constraint_ =\n                      lookup_default_return_constraint(\"\", id),\n                  .config_group_ = lookup_group(\"\", id),\n                  .config_color_ = lookup_color(\"\", id),\n                  .oauth_ = std::move(oauth),\n                  .default_ttl_ = default_ttl,\n                  .overwrite_ttl_ = overwrite_ttl}))\n              .get();\n\n      co_return co_await update_provider_feed(*saf, std::move(discovery));\n    } catch (std::exception const& ex) {\n      std::cerr << \"[GBFS] error initializing feed \" << id << \" (\"\n                << config.url_ << \"): \" << ex.what() << \"\\n\";\n    }\n  }\n\n  awaitable<void> update_provider_feed(\n      provider_feed const& pf,\n      std::optional<gbfs_file> discovery = std::nullopt) {\n    auto& provider = add_provider(pf);\n\n    // check if exists in old data - if so, reuse existing file infos\n    gbfs_provider const* prev_provider = nullptr;\n    if (prev_d_ != nullptr) {\n      if (auto const it = prev_d_->provider_by_id_.find(pf.id_);\n          it != end(prev_d_->provider_by_id_)) {\n        prev_provider = prev_d_->providers_[it->second].get();\n        if (prev_provider != nullptr) {\n          provider.file_infos_ = prev_provider->file_infos_;\n        }\n      }\n    }\n    if (!provider.file_infos_) {\n      provider.file_infos_ = std::make_shared<provider_file_infos>();\n    }\n\n    co_return co_await process_provider_feed(pf, provider, prev_provider,\n                                             std::move(discovery));\n  }\n\n  gbfs_provider& add_provider(provider_feed const& pf) {\n    auto const init_provider = [&](gbfs_provider& provider,\n                                   gbfs_provider_idx_t const idx) {\n      provider.id_ = pf.id_;\n      provider.idx_ = idx;\n      provider.default_restrictions_ = pf.default_restrictions_;\n      provider.default_return_constraint_ = pf.default_return_constraint_;\n      provider.color_ = pf.config_color_;\n      if (pf.config_group_) {\n        provider.group_id_ = *pf.config_group_;\n      }\n    };\n\n    if (auto it = d_->provider_by_id_.find(pf.id_);\n        it != end(d_->provider_by_id_)) {\n      // existing provider, keep idx\n      auto const idx = it->second;\n      assert(d_->providers_.at(idx) == nullptr);\n      d_->providers_[idx] = std::make_unique<gbfs_provider>();\n      auto& provider = *d_->providers_[idx].get();\n      init_provider(provider, idx);\n      return provider;\n    } else {\n      // new provider\n      auto const idx = gbfs_provider_idx_t{d_->providers_.size()};\n      auto& provider =\n          *d_->providers_.emplace_back(std::make_unique<gbfs_provider>()).get();\n      d_->provider_by_id_[pf.id_] = idx;\n      init_provider(provider, idx);\n      return provider;\n    }\n  }\n\n  awaitable<void> process_provider_feed(\n      provider_feed const& pf,\n      gbfs_provider& provider,\n      gbfs_provider const* prev_provider,\n      std::optional<gbfs_file> discovery = std::nullopt) {\n    auto& file_infos = provider.file_infos_;\n    auto data_changed = false;\n    auto geofencing_updated = false;\n\n    try {\n      if (!discovery && needs_refresh(provider.file_infos_->urls_fi_)) {\n        discovery =\n            co_await fetch_file(\"gbfs\", pf.url_, pf.headers_, pf.oauth_,\n                                pf.dir_, pf.default_ttl_, pf.overwrite_ttl_);\n      }\n      if (discovery) {\n        file_infos->urls_ = parse_discovery(discovery->json_);\n        file_infos->urls_fi_.expiry_ = discovery->next_refresh_;\n        file_infos->urls_fi_.hash_ = discovery->hash_;\n      }\n\n      auto const update = [&](std::string_view const name, file_info& fi,\n                              auto const& fn,\n                              bool const force = false) -> awaitable<bool> {\n        if (!file_infos->urls_.contains(name)) {\n          co_return false;\n        }\n        if (force || needs_refresh(fi)) {\n          auto file = co_await fetch_file(name, file_infos->urls_.at(name),\n                                          pf.headers_, pf.oauth_, pf.dir_,\n                                          pf.default_ttl_, pf.overwrite_ttl_);\n          auto const hash_changed = file.hash_ != fi.hash_;\n          auto j_root = file.json_.as_object();\n          fi.expiry_ = file.next_refresh_;\n          fi.hash_ = file.hash_;\n          fn(provider, file.json_);\n          co_return hash_changed;\n        }\n        co_return false;\n      };\n\n      auto const sys_info_updated = co_await update(\n          \"system_information\", file_infos->system_information_fi_,\n          load_system_information);\n      if (!sys_info_updated && prev_provider != nullptr) {\n        provider.sys_info_ = prev_provider->sys_info_;\n      }\n\n      auto const vehicle_types_updated = co_await update(\n          \"vehicle_types\", file_infos->vehicle_types_fi_, load_vehicle_types);\n      if (!vehicle_types_updated && prev_provider != nullptr) {\n        provider.vehicle_types_ = prev_provider->vehicle_types_;\n        provider.vehicle_types_map_ = prev_provider->vehicle_types_map_;\n        provider.temp_vehicle_types_ = prev_provider->temp_vehicle_types_;\n      }\n\n      auto const stations_updated = co_await update(\n          \"station_information\", file_infos->station_information_fi_,\n          load_station_information, vehicle_types_updated);\n      if ((!stations_updated && !vehicle_types_updated) &&\n          prev_provider != nullptr) {\n        provider.stations_ = prev_provider->stations_;\n      }\n\n      auto const station_status_updated = co_await update(\n          \"station_status\", file_infos->station_status_fi_, load_station_status,\n          stations_updated || vehicle_types_updated);\n\n      auto const vehicle_status_updated =\n          co_await update(\"vehicle_status\", file_infos->vehicle_status_fi_,\n                          load_vehicle_status, vehicle_types_updated)  // 3.x\n          || co_await update(\"free_bike_status\", file_infos->vehicle_status_fi_,\n                             load_vehicle_status,\n                             vehicle_types_updated);  // 1.x / 2.x\n      if ((!vehicle_status_updated && !vehicle_types_updated) &&\n          prev_provider != nullptr) {\n        provider.vehicle_status_ = prev_provider->vehicle_status_;\n      }\n\n      geofencing_updated =\n          co_await update(\"geofencing_zones\", file_infos->geofencing_zones_fi_,\n                          load_geofencing_zones, vehicle_types_updated);\n      if ((!geofencing_updated && !vehicle_types_updated) &&\n          prev_provider != nullptr) {\n        provider.geofencing_zones_ = prev_provider->geofencing_zones_;\n      }\n\n      if (prev_provider != nullptr) {\n        provider.has_vehicles_to_rent_ = prev_provider->has_vehicles_to_rent_;\n      }\n\n      if (!provider.color_.has_value() && !provider.sys_info_.color_.empty()) {\n        provider.color_ = provider.sys_info_.color_;\n      }\n\n      auto group_name = std::optional<std::string>{};\n      if (provider.group_id_.empty()) {\n        auto generated_id = provider.sys_info_.name_;\n        std::erase(generated_id, ',');\n        provider.group_id_ = generated_id;\n        group_name = provider.sys_info_.name_;\n      }\n\n      if (auto it = d_->groups_.find(provider.group_id_);\n          it == end(d_->groups_)) {\n        d_->groups_.emplace(\n            provider.group_id_,\n            gbfs_group{.id_ = provider.group_id_,\n                       .name_ = group_name.value_or(provider.group_id_),\n                       .color_ = {},\n                       .providers_ = {provider.idx_}});\n      } else {\n        it->second.providers_.push_back(provider.idx_);\n      }\n\n      if (stations_updated || vehicle_status_updated) {\n        for (auto const& st : provider.stations_ | std::views::values) {\n          provider.bbox_.extend(st.info_.pos_);\n        }\n        for (auto const& vs : provider.vehicle_status_) {\n          provider.bbox_.extend(vs.pos_);\n        }\n      } else if (prev_provider != nullptr) {\n        provider.bbox_ = prev_provider->bbox_;\n      }\n\n      data_changed = vehicle_types_updated || stations_updated ||\n                     station_status_updated || vehicle_status_updated ||\n                     geofencing_updated;\n    } catch (std::exception const& ex) {\n      std::cerr << \"[GBFS] error processing feed \" << pf.id_ << \" (\" << pf.url_\n                << \"): \" << ex.what() << \"\\n\";\n      if (!std::string_view{ex.what()}.starts_with(\"HTTP \")) {\n        if (auto const trace =\n                boost::stacktrace::stacktrace::from_current_exception();\n            trace) {\n          std::cerr << trace << std::endl;\n        }\n      }\n\n      // keep previous data\n      if (prev_provider != nullptr) {\n        provider.sys_info_ = prev_provider->sys_info_;\n        provider.vehicle_types_ = prev_provider->vehicle_types_;\n        provider.vehicle_types_map_ = prev_provider->vehicle_types_map_;\n        provider.temp_vehicle_types_ = prev_provider->temp_vehicle_types_;\n        provider.stations_ = prev_provider->stations_;\n        provider.vehicle_status_ = prev_provider->vehicle_status_;\n        provider.geofencing_zones_ = prev_provider->geofencing_zones_;\n        provider.has_vehicles_to_rent_ = prev_provider->has_vehicles_to_rent_;\n        provider.bbox_ = prev_provider->bbox_;\n      }\n    }\n\n    if (data_changed) {\n      try {\n        partition_provider(provider);\n        provider.has_vehicles_to_rent_ = utl::any_of(\n            provider.products_,\n            [](auto const& prod) { return prod.has_vehicles_to_rent_; });\n\n        update_rtree(provider, prev_provider, geofencing_updated);\n\n        d_->cache_.try_add_or_update(provider.idx_, [&]() {\n          return compute_provider_routing_data(w_, l_, provider);\n        });\n      } catch (std::exception const& ex) {\n        std::cerr << \"[GBFS] error updating provider \" << pf.id_ << \": \"\n                  << ex.what() << \"\\n\";\n      }\n    } else if (prev_provider != nullptr) {\n      // data not changed, copy previously computed products\n      provider.products_ = prev_provider->products_;\n      provider.has_vehicles_to_rent_ = prev_provider->has_vehicles_to_rent_;\n    }\n  }\n\n  void partition_provider(gbfs_provider& provider) {\n    if (provider.vehicle_types_.empty()) {\n      auto& prod = provider.products_.emplace_back();\n      prod.idx_ = gbfs_products_idx_t{0};\n      prod.has_vehicles_to_rent_ =\n          utl::any_of(provider.stations_,\n                      [](auto const& st) {\n                        return st.second.status_.is_renting_ &&\n                               st.second.status_.num_vehicles_available_ > 0;\n                      }) ||\n          utl::any_of(provider.vehicle_status_, [](auto const& vs) {\n            return !vs.is_disabled_ && !vs.is_reserved_;\n          });\n    } else {\n      auto part = partition{vehicle_type_idx_t{provider.vehicle_types_.size()}};\n\n      // refine by form factor + propulsion type\n      auto by_form_factor =\n          hash_map<std::pair<vehicle_form_factor, propulsion_type>,\n                   std::vector<vehicle_type_idx_t>>{};\n      for (auto const& vt : provider.vehicle_types_) {\n        by_form_factor[std::pair{vt.form_factor_, vt.propulsion_type_}]\n            .push_back(vt.idx_);\n      }\n      for (auto const& [_, vt_indices] : by_form_factor) {\n        part.refine(vt_indices);\n      }\n\n      // refine by return constraints\n      auto by_return_constraint =\n          hash_map<return_constraint, std::vector<vehicle_type_idx_t>>{};\n      for (auto const& vt : provider.vehicle_types_) {\n        by_return_constraint[vt.return_constraint_].push_back(vt.idx_);\n      }\n      for (auto const& [_, vt_indices] : by_return_constraint) {\n        part.refine(vt_indices);\n      }\n\n      // refine by known vs. guessed return constraints\n      auto known_return_constraints = std::vector<vehicle_type_idx_t>{};\n      auto guessed_return_constraints = std::vector<vehicle_type_idx_t>{};\n      for (auto const& vt : provider.vehicle_types_) {\n        if (vt.known_return_constraint_) {\n          known_return_constraints.push_back(vt.idx_);\n        } else {\n          guessed_return_constraints.push_back(vt.idx_);\n        }\n      }\n      part.refine(known_return_constraints);\n      part.refine(guessed_return_constraints);\n\n      // refine by return stations\n      // TODO: only do this if the station is not in a zone where vehicles\n      //   can be returned anywhere\n      auto vts = std::vector<vehicle_type_idx_t>{};\n      for (auto const& [id, st] : provider.stations_) {\n        if (!st.status_.vehicle_docks_available_.empty()) {\n          vts.clear();\n          for (auto const& [vt, num] : st.status_.vehicle_docks_available_) {\n            vts.push_back(vt);\n          }\n          part.refine(vts);\n        }\n      }\n\n      // refine by geofencing zones\n      for (auto const& z : provider.geofencing_zones_.zones_) {\n        for (auto const& r : z.rules_) {\n          part.refine(r.vehicle_type_idxs_);\n        }\n      }\n\n      for (auto const& set : part.get_sets()) {\n        auto const prod_idx = gbfs_products_idx_t{provider.products_.size()};\n        auto& prod = provider.products_.emplace_back();\n        prod.idx_ = prod_idx;\n        prod.vehicle_types_ = set;\n        auto const first_vt =\n            provider.vehicle_types_.at(prod.vehicle_types_.front());\n        prod.form_factor_ = first_vt.form_factor_;\n        prod.propulsion_type_ = first_vt.propulsion_type_;\n        prod.return_constraint_ = first_vt.return_constraint_;\n        prod.known_return_constraint_ = first_vt.known_return_constraint_;\n        prod.has_vehicles_to_rent_ =\n            utl::any_of(provider.stations_,\n                        [&](auto const& st) {\n                          return st.second.status_.is_renting_ &&\n                                 st.second.status_.num_vehicles_available_ > 0;\n                        }) ||\n            utl::any_of(provider.vehicle_status_, [&](auto const& vs) {\n              return !vs.is_disabled_ && !vs.is_reserved_ &&\n                     prod.includes_vehicle_type(vs.vehicle_type_idx_);\n            });\n      }\n    }\n  }\n\n  void update_rtree(gbfs_provider const& provider,\n                    gbfs_provider const* prev_provider,\n                    bool const zones_changed) {\n    auto added_stations = 0U;\n    auto added_vehicles = 0U;\n    auto removed_stations = 0U;\n    auto removed_vehicles = 0U;\n    auto moved_stations = 0U;\n    auto moved_vehicles = 0U;\n\n    if (prev_provider != nullptr) {\n      using ST = std::pair<std::string, station>;\n      utl::sorted_diff(\n          prev_provider->stations_, provider.stations_,\n          [](ST const& a, ST const& b) { return a.first < b.first; },\n          [](ST const& a, ST const& b) {\n            return a.second.info_.pos_ == b.second.info_.pos_;\n          },\n          utl::overloaded{\n              [&](utl::op const o, ST const& s) {\n                if (o == utl::op::kAdd) {\n                  d_->provider_rtree_.add(s.second.info_.pos_, provider.idx_);\n                  ++added_stations;\n                } else {  // del\n                  d_->provider_rtree_.remove(s.second.info_.pos_,\n                                             provider.idx_);\n                  ++removed_stations;\n                }\n              },\n              [&](ST const& a, ST const& b) {\n                d_->provider_rtree_.remove(a.second.info_.pos_, provider.idx_);\n                d_->provider_rtree_.add(b.second.info_.pos_, provider.idx_);\n                ++moved_stations;\n              }});\n      utl::sorted_diff(\n          prev_provider->vehicle_status_, provider.vehicle_status_,\n          [](vehicle_status const& a, vehicle_status const& b) {\n            return a.id_ < b.id_;\n          },\n          [](vehicle_status const& a, vehicle_status const& b) {\n            return a.pos_ == b.pos_;\n          },\n          utl::overloaded{\n              [&](utl::op const o, vehicle_status const& v) {\n                if (o == utl::op::kAdd) {\n                  d_->provider_rtree_.add(v.pos_, provider.idx_);\n                  ++added_vehicles;\n                } else {  // del\n                  d_->provider_rtree_.remove(v.pos_, provider.idx_);\n                  ++removed_vehicles;\n                }\n              },\n              [&](vehicle_status const& a, vehicle_status const& b) {\n                d_->provider_rtree_.remove(a.pos_, provider.idx_);\n                d_->provider_rtree_.add(b.pos_, provider.idx_);\n                ++moved_vehicles;\n              }});\n      if (zones_changed) {\n        for (auto const& zone : prev_provider->geofencing_zones_.zones_) {\n          d_->provider_zone_rtree_.remove(zone.bounding_box(), provider.idx_);\n        }\n        for (auto const& zone : provider.geofencing_zones_.zones_) {\n          if (zone.allows_rental_operation()) {\n            d_->provider_zone_rtree_.add(zone.bounding_box(), provider.idx_);\n          }\n        }\n      }\n    } else {\n      for (auto const& station : provider.stations_) {\n        d_->provider_rtree_.add(station.second.info_.pos_, provider.idx_);\n        ++added_stations;\n      }\n      for (auto const& vehicle : provider.vehicle_status_) {\n        if (vehicle.station_id_.empty()) {\n          d_->provider_rtree_.add(vehicle.pos_, provider.idx_);\n          ++added_vehicles;\n        }\n      }\n      for (auto const& zone : provider.geofencing_zones_.zones_) {\n        if (zone.allows_rental_operation()) {\n          d_->provider_zone_rtree_.add(zone.bounding_box(), provider.idx_);\n        }\n      }\n    }\n  }\n\n  awaitable<void> init_aggregated_feed(\n      std::string const& prefix,\n      std::string const& url,\n      headers_t const& headers,\n      std::shared_ptr<oauth_state>&& oauth,\n      boost::json::object const& root,\n      std::map<std::string, unsigned> const& default_ttl = {},\n      std::map<std::string, unsigned> const& overwrite_ttl = {}) {\n    auto af =\n        d_->aggregated_feeds_\n            ->emplace_back(std::make_unique<aggregated_feed>(aggregated_feed{\n                .id_ = prefix,\n                .url_ = url,\n                .headers_ = headers,\n                .expiry_ = get_expiry(root, std::chrono::hours{1}, default_ttl,\n                                      overwrite_ttl, \"manifest\"),\n                .oauth_ = std::move(oauth),\n                .default_ttl_ = default_ttl,\n                .overwrite_ttl_ = overwrite_ttl}))\n            .get();\n\n    co_return co_await process_aggregated_feed(*af, root);\n  }\n\n  awaitable<void> update_aggregated_feed(aggregated_feed& af) {\n    if (af.needs_update()) {\n      auto const file =\n          co_await fetch_file(\"manifest\", af.url_, af.headers_, af.oauth_,\n                              std::nullopt, af.default_ttl_, af.overwrite_ttl_);\n      co_await process_aggregated_feed(af, file.json_.as_object());\n    } else {\n      co_await update_aggregated_feed_provider_feeds(af);\n    }\n  }\n\n  awaitable<void> process_aggregated_feed(aggregated_feed& af,\n                                          boost::json::object const& root) {\n    auto feeds = std::vector<provider_feed>{};\n    if (root.contains(\"data\") &&\n        root.at(\"data\").as_object().contains(\"datasets\")) {\n      // GBFS 3.x manifest.json\n      for (auto const& dataset : root.at(\"data\").at(\"datasets\").as_array()) {\n        auto const system_id =\n            static_cast<std::string>(dataset.at(\"system_id\").as_string());\n        auto const combined_id = fmt::format(\"{}:{}\", af.id_, system_id);\n\n        auto const& versions = dataset.at(\"versions\").as_array();\n        if (versions.empty()) {\n          continue;\n        }\n        // versions array must be sorted by increasing version number\n        auto const& latest_version = versions.back().as_object();\n        feeds.emplace_back(provider_feed{\n            .id_ = combined_id,\n            .url_ =\n                static_cast<std::string>(latest_version.at(\"url\").as_string()),\n            .headers_ = af.headers_,\n            .default_restrictions_ =\n                lookup_default_restrictions(af.id_, combined_id),\n            .default_return_constraint_ =\n                lookup_default_return_constraint(af.id_, combined_id),\n            .config_group_ = lookup_group(af.id_, system_id),\n            .config_color_ = lookup_color(af.id_, system_id),\n            .oauth_ = af.oauth_,\n            .default_ttl_ = af.default_ttl_,\n            .overwrite_ttl_ = af.overwrite_ttl_});\n      }\n    } else if (root.contains(\"systems\")) {\n      // Lamassu 2.3 format\n      for (auto const& system : root.at(\"systems\").as_array()) {\n        auto const system_id =\n            static_cast<std::string>(system.at(\"id\").as_string());\n        auto const combined_id = fmt::format(\"{}:{}\", af.id_, system_id);\n        feeds.emplace_back(provider_feed{\n            .id_ = combined_id,\n            .url_ = static_cast<std::string>(system.at(\"url\").as_string()),\n            .headers_ = af.headers_,\n            .default_restrictions_ =\n                lookup_default_restrictions(af.id_, combined_id),\n            .default_return_constraint_ =\n                lookup_default_return_constraint(af.id_, combined_id),\n            .config_group_ = lookup_group(af.id_, system_id),\n            .config_color_ = lookup_color(af.id_, system_id),\n            .oauth_ = af.oauth_,\n            .default_ttl_ = af.default_ttl_,\n            .overwrite_ttl_ = af.overwrite_ttl_});\n      }\n    }\n\n    af.feeds_ = std::move(feeds);\n    co_await update_aggregated_feed_provider_feeds(af);\n  }\n\n  awaitable<void> update_aggregated_feed_provider_feeds(aggregated_feed& af) {\n    auto executor = co_await asio::this_coro::executor;\n    co_await asio::experimental::make_parallel_group(\n        utl::to_vec(af.feeds_,\n                    [&](auto const& pf) {\n                      return boost::asio::co_spawn(\n                          executor,\n                          [this, pf = &pf]() -> awaitable<void> {\n                            co_await update_provider_feed(*pf);\n                          },\n                          asio::deferred);\n                    }))\n        .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable);\n  }\n\n  awaitable<gbfs_file> fetch_file(\n      std::string_view const name,\n      std::string_view const url,\n      headers_t const& base_headers,\n      std::shared_ptr<oauth_state> const& oauth,\n      std::optional<std::filesystem::path> const& dir = std::nullopt,\n      std::map<std::string, unsigned> const& default_ttl = {},\n      std::map<std::string, unsigned> const& overwrite_ttl = {}) {\n    auto content = std::string{};\n    if (dir.has_value()) {\n      content = read_file(*dir / fmt::format(\"{}.json\", name));\n    } else {\n      auto headers = base_headers;\n      co_await get_oauth_token(oauth, headers);\n      auto const res = co_await http_GET(boost::urls::url{url},\n                                         std::move(headers), timeout_, proxy_);\n      content = get_http_body(res);\n      if (res.result_int() != 200) {\n        throw std::runtime_error(\n            fmt::format(\"HTTP {} fetching {}\", res.result_int(), url));\n      }\n    }\n    auto j = json::parse(content);\n    auto j_root = j.as_object();\n    auto const next_refresh = get_expiry(j_root, std::chrono::seconds{0},\n                                         default_ttl, overwrite_ttl, name);\n    co_return gbfs_file{.json_ = std::move(j),\n                        .hash_ = hash_gbfs_data(content),\n                        .next_refresh_ = next_refresh};\n  }\n\n  awaitable<void> get_oauth_token(std::shared_ptr<oauth_state> const& oauth,\n                                  headers_t& headers,\n                                  std::chrono::seconds remaining_time_required =\n                                      std::chrono::seconds{120}) {\n    if (oauth == nullptr || oauth->settings_.token_url_.empty()) {\n      co_return;\n    }\n    co_await refresh_oauth_token(oauth, remaining_time_required);\n    headers[\"Authorization\"] = fmt::format(\"Bearer {}\", oauth->access_token_);\n  }\n\n  awaitable<void> refresh_oauth_token(\n      std::shared_ptr<oauth_state> const& oauth,\n      std::chrono::seconds remaining_time_required) {\n    if (oauth == nullptr || oauth->settings_.token_url_.empty()) {\n      co_return;\n    }\n    if (!oauth->access_token_.empty() && oauth->expiry_.has_value() &&\n        (*oauth->expiry_ - std::chrono::system_clock::now()) >\n            remaining_time_required) {\n      // token still valid\n      co_return;\n    }\n    try {\n      auto const opt = boost::urls::encoding_opts(true);\n      auto const body = fmt::format(\n          \"grant_type=client_credentials&client_id={}&client_secret={}\",\n          boost::urls::encode(oauth->settings_.client_id_,\n                              boost::urls::unreserved_chars, opt),\n          boost::urls::encode(oauth->settings_.client_secret_,\n                              boost::urls::unreserved_chars, opt));\n      auto oauth_headers = oauth->settings_.headers_.value_or(headers_t{});\n      oauth_headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n\n      auto const res =\n          co_await http_POST(boost::urls::url{oauth->settings_.token_url_},\n                             std::move(oauth_headers), body, timeout_);\n      auto const res_body = get_http_body(res);\n      auto const res_json = json::parse(res_body);\n      auto const& j = res_json.as_object();\n\n      if (res.result_int() != 200) {\n        std::cerr << \"[GBFS] oauth token request failed: \";\n        if (j.contains(\"error\")) {\n          std::cerr << j.at(\"error\").as_string();\n        } else {\n          std::cerr << \"HTTP \" << res.result_int();\n        }\n        if (j.contains(\"error_description\")) {\n          std::cerr << \" (\" << j.at(\"error_description\").as_string() << \")\";\n        }\n        if (j.contains(\"error_uri\")) {\n          std::cerr << \" (\" << j.at(\"error_uri\").as_string() << \")\";\n        }\n        std::cerr << \" (token url: \" << oauth->settings_.token_url_ << \")\"\n                  << std::endl;\n        throw std::runtime_error(\"oauth token request failed\");\n      }\n\n      auto const token_type = j.at(\"token_type\").as_string();\n      utl::verify(token_type == \"Bearer\", \"unsupported oauth token type \\\"{}\\\"\",\n                  token_type);\n      oauth->access_token_ =\n          static_cast<std::string>(j.at(\"access_token\").as_string());\n\n      oauth->expires_in_ = oauth->settings_.expires_in_.value_or(60 * 60 * 24);\n      if (j.contains(\"expires_in\")) {\n        oauth->expires_in_ = std::min(oauth->expires_in_,\n                                      j.at(\"expires_in\").to_number<unsigned>());\n      }\n      oauth->expiry_ = std::chrono::system_clock::now() +\n                       std::chrono::seconds{oauth->expires_in_};\n    } catch (std::runtime_error const& e) {\n      std::cerr << \"[GBFS] oauth token request error: \" << e.what()\n                << std::endl;\n      throw;\n    }\n  }\n\n  awaitable<void> refresh_oauth_tokens() {\n    auto states = std::set<std::shared_ptr<oauth_state>>{};\n    for (auto const& af : *d_->aggregated_feeds_) {\n      if (af->oauth_ != nullptr) {\n        states.insert(af->oauth_);\n      }\n    }\n    for (auto const& pf : *d_->standalone_feeds_) {\n      if (pf->oauth_ != nullptr) {\n        states.insert(pf->oauth_);\n      }\n    }\n\n    if (states.empty()) {\n      // this is necessary, because calling async_wait on an empty group\n      // causes everything to break\n      co_return;\n    }\n\n    auto executor = co_await asio::this_coro::executor;\n    co_await asio::experimental::make_parallel_group(\n        utl::to_vec(states,\n                    [&](auto const& state) {\n                      return boost::asio::co_spawn(\n                          executor,\n                          [this, state]() -> awaitable<void> {\n                            co_await refresh_oauth_token(\n                                state,\n                                std::chrono::seconds{state->expires_in_ / 2});\n                          },\n                          asio::deferred);\n                    }))\n        .async_wait(asio::experimental::wait_for_all(), asio::use_awaitable);\n  }\n\n  geofencing_restrictions lookup_default_restrictions(std::string const& prefix,\n                                                      std::string const& id) {\n    auto const convert = [&](config::gbfs::restrictions const& r) {\n      return geofencing_restrictions{\n          .ride_start_allowed_ = r.ride_start_allowed_,\n          .ride_end_allowed_ = r.ride_end_allowed_,\n          .ride_through_allowed_ = r.ride_through_allowed_,\n          .station_parking_ = r.station_parking_};\n    };\n\n    if (auto const it = c_.default_restrictions_.find(id);\n        it != end(c_.default_restrictions_)) {\n      return convert(it->second);\n    } else if (auto const prefix_it = c_.default_restrictions_.find(prefix);\n               prefix_it != end(c_.default_restrictions_)) {\n      return convert(prefix_it->second);\n    } else {\n      return {};\n    }\n  }\n\n  std::optional<return_constraint> lookup_default_return_constraint(\n      std::string const& prefix, std::string const& id) {\n    auto const convert = [&](config::gbfs::restrictions const& r) {\n      return r.return_constraint_.has_value()\n                 ? parse_return_constraint(r.return_constraint_.value())\n                 : std::nullopt;\n    };\n    if (auto const it = c_.default_restrictions_.find(id);\n        it != end(c_.default_restrictions_)) {\n      return convert(it->second);\n    } else if (auto const prefix_it = c_.default_restrictions_.find(prefix);\n               prefix_it != end(c_.default_restrictions_)) {\n      return convert(prefix_it->second);\n    } else {\n      return {};\n    }\n  }\n\n  template <typename Getter>\n  std::optional<std::string> lookup_mapping(std::string const& af_id,\n                                            std::string const& system_id,\n                                            Getter getter) {\n    auto const& af_config = c_.feeds_.at(af_id.empty() ? system_id : af_id);\n    auto const& opt = getter(af_config);\n    if (opt.has_value()) {\n      return std::visit(\n          utl::overloaded{\n              [&](std::string const& s) -> std::optional<std::string> {\n                return std::optional{s};\n              },\n              [&](std::map<std::string, std::string> const& m)\n                  -> std::optional<std::string> {\n                if (auto const it = m.find(system_id); it != end(m)) {\n                  return std::optional{it->second};\n                }\n                return {};\n              }},\n          *opt);\n    }\n    return {};\n  }\n\n  std::optional<std::string> lookup_group(std::string const& af_id,\n                                          std::string const& system_id) {\n    return lookup_mapping(af_id, system_id,\n                          [](auto const& cfg) { return cfg.group_; });\n  }\n\n  std::optional<std::string> lookup_color(std::string const& af_id,\n                                          std::string const& system_id) {\n    return lookup_mapping(af_id, system_id,\n                          [](auto const& cfg) { return cfg.color_; });\n  }\n\n  config::gbfs const& c_;\n  osr::ways const& w_;\n  osr::lookup const& l_;\n\n  gbfs_data* d_;\n  gbfs_data const* prev_d_;\n\n  std::chrono::seconds timeout_;\n  std::optional<proxy> proxy_;\n};\n\nawaitable<void> update(config const& c,\n                       osr::ways const& w,\n                       osr::lookup const& l,\n                       std::shared_ptr<gbfs_data>& data_ptr) {\n  auto const t = utl::scoped_timer{\"gbfs::update\"};\n\n  if (!c.gbfs_.has_value()) {\n    co_return;\n  }\n\n  auto const prev_d = data_ptr;\n  auto const d = std::make_shared<gbfs_data>(c.gbfs_->cache_size_);\n\n  auto update = gbfs_update{*c.gbfs_, w, l, d.get(), prev_d.get()};\n  try {\n    co_await update.run();\n  } catch (std::exception const& e) {\n    std::cerr << \"[GBFS] update error: \" << e.what() << std::endl;\n    if (auto const trace =\n            boost::stacktrace::stacktrace::from_current_exception();\n        trace) {\n      std::cerr << trace << std::endl;\n    }\n  }\n  data_ptr = d;\n}\n\nvoid run_gbfs_update(boost::asio::io_context& ioc,\n                     config const& c,\n                     osr::ways const& w,\n                     osr::lookup const& l,\n                     std::shared_ptr<gbfs_data>& data_ptr) {\n  boost::asio::co_spawn(\n      ioc,\n      [&]() -> awaitable<void> {\n        auto executor = co_await asio::this_coro::executor;\n        auto timer = asio::steady_timer{executor};\n        auto ec = boost::system::error_code{};\n        auto cc = c;\n\n        while (true) {\n          // Remember when we started so we can schedule the next update.\n          auto const start = std::chrono::steady_clock::now();\n\n          co_await update(cc, w, l, data_ptr);\n\n          // Schedule next update.\n          timer.expires_at(start +\n                           std::chrono::seconds{cc.gbfs_->update_interval_});\n          co_await timer.async_wait(\n              asio::redirect_error(asio::use_awaitable, ec));\n          if (ec == asio::error::operation_aborted) {\n            co_return;\n          }\n        }\n      },\n      boost::asio::detached);\n}\n\n}  // namespace motis::gbfs\n"
  },
  {
    "path": "src/get_stops_with_traffic.cc",
    "content": "#include \"motis/get_stops_with_traffic.h\"\n\n#include \"osr/location.h\"\n\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/timetable.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nstd::vector<n::location_idx_t> get_stops_with_traffic(\n    n::timetable const& tt,\n    n::rt_timetable const* rtt,\n    point_rtree<n::location_idx_t> const& rtree,\n    osr::location const& pos,\n    double const distance,\n    n::location_idx_t const not_equal_to) {\n  auto ret = std::vector<n::location_idx_t>{};\n  rtree.in_radius(pos.pos_, distance, [&](n::location_idx_t const l) {\n    if (tt.location_routes_[l].empty() &&\n        (rtt == nullptr || rtt->location_rt_transports_[l].empty())) {\n      return;\n    }\n    if (l == not_equal_to) {\n      return;\n    }\n    ret.emplace_back(l);\n  });\n  return ret;\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/hashes.cc",
    "content": "#include \"motis/hashes.h\"\n\n#include <fstream>\n#include <ostream>\n\n#include \"boost/json.hpp\"\n\n#include \"cista/mmap.h\"\n\nnamespace fs = std::filesystem;\n\nnamespace motis {\n\nstd::string to_str(meta_t const& h) {\n  return boost::json::serialize(boost::json::value_from(h));\n}\n\nmeta_t read_hashes(fs::path const& data_path, std::string const& name) {\n  auto const p = (data_path / \"meta\" / (name + \".json\"));\n  if (!exists(p)) {\n    return {};\n  }\n  auto const mmap =\n      cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ};\n  return boost::json::value_to<meta_t>(boost::json::parse(mmap.view()));\n}\n\nvoid write_hashes(fs::path const& data_path,\n                  std::string const& name,\n                  meta_t const& h) {\n  auto const p = (data_path / \"meta\" / (name + \".json\"));\n  std::ofstream{p} << to_str(h);\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/http_req.cc",
    "content": "#include \"motis/http_req.h\"\n\n#include \"boost/asio/awaitable.hpp\"\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/asio/ssl.hpp\"\n#include \"boost/beast/core.hpp\"\n#include \"boost/beast/http.hpp\"\n#include \"boost/beast/http/dynamic_body.hpp\"\n#include \"boost/beast/ssl/ssl_stream.hpp\"\n#include \"boost/beast/version.hpp\"\n#include \"boost/iostreams/copy.hpp\"\n#include \"boost/iostreams/filter/gzip.hpp\"\n#include \"boost/iostreams/filtering_stream.hpp\"\n#include \"boost/iostreams/filtering_streambuf.hpp\"\n#include \"boost/url/url.hpp\"\n\n#include \"utl/verify.h\"\n\nnamespace motis {\n\nnamespace beast = boost::beast;\nnamespace http = beast::http;\nnamespace asio = boost::asio;\nnamespace ssl = asio::ssl;\n\ntemplate <typename Stream>\nasio::awaitable<http_response> req(\n    Stream&&,\n    boost::urls::url const&,\n    std::map<std::string, std::string> const&,\n    std::optional<std::string> const& body = std::nullopt);\n\nasio::awaitable<http_response> req_no_tls(\n    boost::urls::url const& url,\n    std::map<std::string, std::string> const& headers,\n    std::optional<std::string> const& body,\n    std::chrono::seconds const timeout,\n    std::optional<proxy> const& proxy) {\n  auto executor = co_await asio::this_coro::executor;\n  auto resolver = asio::ip::tcp::resolver{executor};\n  auto stream = beast::tcp_stream{executor};\n\n  auto const host = proxy ? proxy->host_ : url.host();\n  auto const port =\n      proxy ? proxy->port_ : std::string{url.has_port() ? url.port() : \"80\"};\n  auto const results = co_await resolver.async_resolve(host, port);\n\n  stream.expires_after(timeout);\n\n  co_await stream.async_connect(results);\n  co_return co_await req(std::move(stream), url, headers, body);\n}\n\nasio::awaitable<http_response> req_tls(\n    boost::urls::url const& url,\n    std::map<std::string, std::string> const& headers,\n    std::optional<std::string> const& body,\n    std::chrono::seconds const timeout,\n    std::optional<proxy> const& proxy) {\n  auto ssl_ctx = ssl::context{ssl::context::tlsv12_client};\n  ssl_ctx.set_default_verify_paths();\n  ssl_ctx.set_verify_mode(ssl::verify_none);\n  ssl_ctx.set_options(ssl::context::default_workarounds |\n                      ssl::context::no_sslv2 | ssl::context::no_sslv3 |\n                      ssl::context::single_dh_use);\n\n  auto executor = co_await asio::this_coro::executor;\n  auto resolver = asio::ip::tcp::resolver{executor};\n  auto stream = ssl::stream<beast::tcp_stream>{executor, ssl_ctx};\n\n  auto const host = proxy ? proxy->host_ : url.host();\n  auto const port =\n      proxy ? proxy->port_ : std::string{url.has_port() ? url.port() : \"443\"};\n\n  if (!SSL_set_tlsext_host_name(stream.native_handle(),\n                                const_cast<char*>(host.c_str()))) {\n    throw boost::system::system_error{\n        {static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category()}};\n  }\n\n  auto const results = co_await resolver.async_resolve(host, port);\n\n  stream.next_layer().expires_after(timeout);\n\n  co_await beast::get_lowest_layer(stream).async_connect(results);\n  co_await stream.async_handshake(ssl::stream_base::client);\n  co_return co_await req(std::move(stream), url, headers, body);\n}\n\ntemplate <typename Stream>\nasio::awaitable<http_response> req(\n    Stream&& stream,\n    boost::urls::url const& url,\n    std::map<std::string, std::string> const& headers,\n    std::optional<std::string> const& body) {\n  auto req = http::request<http::string_body>{\n      body ? http::verb::post : http::verb::get, url.encoded_target(), 11};\n  req.set(http::field::host, url.host());\n  req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);\n  req.set(http::field::accept_encoding, \"gzip\");\n  for (auto const& [k, v] : headers) {\n    req.set(k, v);\n  }\n\n  if (body) {\n    req.body() = *body;\n    req.prepare_payload();\n  }\n\n  co_await http::async_write(stream, req);\n\n  auto p = http::response_parser<http::dynamic_body>{};\n  p.eager(true);\n  p.body_limit(kBodySizeLimit);\n\n  auto buffer = beast::flat_buffer{};\n  co_await http::async_read(stream, buffer, p);\n\n  auto ec = beast::error_code{};\n  beast::get_lowest_layer(stream).socket().shutdown(\n      asio::ip::tcp::socket::shutdown_both, ec);\n  co_return p.release();\n}\n\nasio::awaitable<http::response<http::dynamic_body>> http_GET(\n    boost::urls::url url,\n    std::map<std::string, std::string> const& headers,\n    std::chrono::seconds const timeout,\n    std::optional<proxy> const& proxy) {\n  auto n_redirects = 0U;\n  auto next_url = url;\n  while (n_redirects < 3U) {\n    auto const use_tls =\n        proxy.has_value() ? proxy->use_tls_\n                          : next_url.scheme_id() == boost::urls::scheme::https;\n    auto const res = co_await (\n        use_tls ? req_tls(next_url, headers, std::nullopt, timeout, proxy)\n                : req_no_tls(next_url, headers, std::nullopt, timeout, proxy));\n    auto const code = res.base().result_int();\n    if (code >= 300 && code < 400) {\n      next_url = boost::urls::url{res.base()[\"Location\"]};\n      ++n_redirects;\n      continue;\n    } else {\n      co_return res;\n    }\n  }\n  throw utl::fail(R\"(too many redirects: \"{}\", latest=\"{}\")\",\n                  fmt::streamed(url), fmt::streamed(next_url));\n}\n\nasio::awaitable<http::response<http::dynamic_body>> http_POST(\n    boost::urls::url url,\n    std::map<std::string, std::string> const& headers,\n    std::string const& body,\n    std::chrono::seconds timeout,\n    std::optional<proxy> const& proxy) {\n  auto n_redirects = 0U;\n  auto next_url = url;\n  while (n_redirects < 3U) {\n    auto const use_tls =\n        proxy.has_value() ? proxy->use_tls_\n                          : next_url.scheme_id() == boost::urls::scheme::https;\n    auto const res = co_await (\n        use_tls ? req_tls(next_url, headers, body, timeout, proxy)\n                : req_no_tls(next_url, headers, body, timeout, proxy));\n    auto const code = res.base().result_int();\n    if (code >= 300 && code < 400) {\n      next_url = boost::urls::url{res.base()[\"Location\"]};\n      ++n_redirects;\n      continue;\n    } else {\n      co_return res;\n    }\n  }\n  throw utl::fail(R\"(too many redirects: \"{}\", latest=\"{}\")\",\n                  fmt::streamed(url), fmt::streamed(next_url));\n}\n\nstd::string get_http_body(http_response const& res) {\n  auto body = beast::buffers_to_string(res.body().data());\n  if (res[http::field::content_encoding] == \"gzip\") {\n    auto const src = boost::iostreams::array_source{body.data(), body.size()};\n    auto is = boost::iostreams::filtering_istream{};\n    auto os = std::stringstream{};\n    is.push(boost::iostreams::gzip_decompressor{});\n    is.push(src);\n    boost::iostreams::copy(is, os);\n    body = os.str();\n  }\n  return body;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/import.cc",
    "content": "#include \"motis/import.h\"\n\n#include <fstream>\n#include <map>\n#include <ostream>\n#include <tuple>\n#include <vector>\n\n#include \"fmt/ranges.h\"\n\n#include \"cista/free_self_allocated.h\"\n#include \"cista/io.h\"\n\n#include \"adr/area_database.h\"\n\n#include \"utl/erase_if.h\"\n#include \"utl/read_file.h\"\n#include \"utl/to_vec.h\"\n\n#include \"tiles/db/clear_database.h\"\n#include \"tiles/db/feature_inserter_mt.h\"\n#include \"tiles/db/feature_pack.h\"\n#include \"tiles/db/pack_file.h\"\n#include \"tiles/db/prepare_tiles.h\"\n#include \"tiles/db/tile_database.h\"\n#include \"tiles/osm/load_coastlines.h\"\n#include \"tiles/osm/load_osm.h\"\n\n#include \"nigiri/loader/assistance.h\"\n#include \"nigiri/loader/load.h\"\n#include \"nigiri/loader/loader_interface.h\"\n#include \"nigiri/clasz.h\"\n#include \"nigiri/common/parse_date.h\"\n#include \"nigiri/routing/tb/preprocess.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/shapes_storage.h\"\n#include \"nigiri/timetable.h\"\n#include \"nigiri/timetable_metrics.h\"\n\n#include \"osr/extract/extract.h\"\n#include \"osr/lookup.h\"\n#include \"osr/ways.h\"\n\n#include \"adr/adr.h\"\n#include \"adr/formatter.h\"\n#include \"adr/reverse.h\"\n#include \"adr/typeahead.h\"\n\n#include \"motis/adr_extend_tt.h\"\n#include \"motis/clog_redirect.h\"\n#include \"motis/compute_footpaths.h\"\n#include \"motis/data.h\"\n#include \"motis/hashes.h\"\n#include \"motis/route_shapes.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/tt_location_rtree.h\"\n\nnamespace fs = std::filesystem;\nnamespace n = nigiri;\nnamespace nl = nigiri::loader;\nusing namespace std::string_literals;\nusing std::chrono_literals::operator\"\"min;\nusing std::chrono_literals::operator\"\"h;\n\nnamespace motis {\n\nstruct task {\n  friend std::ostream& operator<<(std::ostream& out, task const& t) {\n    out << t.name_ << \" \";\n    if (!t.should_run_) {\n      out << \"disabled\";\n    } else if (t.done_) {\n      out << \"done\";\n    } else if (utl::all_of(t.dependencies_,\n                           [](task const* t) { return t->done_; })) {\n      out << \"ready\";\n    } else {\n      out << \"waiting for {\";\n      auto first = true;\n      for (auto const& dep : t.dependencies_) {\n        if (dep->done_) {\n          continue;\n        }\n\n        if (!first) {\n          out << \", \";\n        }\n        first = false;\n        out << dep->name_;\n      }\n      out << \"}\";\n    }\n    return out;\n  }\n\n  bool ready_for_load(fs::path const& data_path) {\n    auto const existing = read_hashes(data_path, name_);\n    if (existing != hashes_) {\n      std::cout << name_ << \"\\n\"\n                << \"  existing: \" << to_str(existing) << \"\\n\"\n                << \"  current: \" << to_str(hashes_) << \"\\n\";\n    }\n    return existing == hashes_;\n  }\n\n  void run(fs::path const& data_path) {\n    auto const pt = utl::activate_progress_tracker(name_);\n    auto const redirect = clog_redirect{\n        (data_path / \"logs\" / (name_ + \".txt\")).generic_string().c_str()};\n    run_();\n    write_hashes(data_path, name_, hashes_);\n    pt->out_ = 100;\n    pt->status(\"FINISHED\");\n    done_ = true;\n  }\n\n  std::string name_;\n  std::vector<task*> dependencies_;\n  bool should_run_;\n  std::function<void()> run_;\n  meta_t hashes_;\n  bool done_{false};\n  utl::progress_tracker_ptr pt_{};\n};\n\n}  // namespace motis\n\ntemplate <>\nstruct fmt::formatter<motis::task> : fmt::ostream_formatter {};\n\nnamespace motis {\n\ncista::hash_t hash_file(fs::path const& p) {\n  auto const str = p.generic_string();\n  if (str.starts_with(\"\\nfunction\") || str.starts_with(\"\\n#\")) {\n    return cista::hash(str);\n  } else if (fs::is_directory(p)) {\n    auto h = cista::BASE_HASH;\n    auto entries = std::vector<std::tuple<std::string, std::uint64_t,\n                                          std::filesystem::file_time_type>>{};\n    for (auto const& entry : fs::recursive_directory_iterator{p}) {\n      auto ec = std::error_code{};\n      entries.emplace_back(fs::relative(entry.path(), p, ec).generic_string(),\n                           entry.is_regular_file(ec) ? entry.file_size(ec) : 0U,\n                           fs::last_write_time(entry.path(), ec));\n    }\n    utl::sort(entries);\n    for (auto const& [rel, size, modified_ts] : entries) {\n      h = cista::hash_combine(h, cista::hash(rel), size,\n                              modified_ts.time_since_epoch().count());\n    }\n    return h;\n  } else {\n    auto const mmap = cista::mmap{str.c_str(), cista::mmap::protection::READ};\n    return cista::hash_combine(\n        cista::hash(mmap.view().substr(\n            0U, std::min(mmap.size(),\n                         static_cast<std::size_t>(50U * 1024U * 1024U)))),\n        mmap.size());\n  }\n}\n\nvoid import(config const& c,\n            fs::path const& data_path,\n            std::optional<std::vector<std::string>> const& task_filter) {\n  c.verify_input_files_exist();\n\n  auto ec = std::error_code{};\n  fs::create_directories(data_path / \"logs\", ec);\n  fs::create_directories(data_path / \"meta\", ec);\n  {\n    auto cfg = std::ofstream{(data_path / \"config.yml\").generic_string()};\n    cfg.exceptions(std::ios_base::badbit | std::ios_base::eofbit);\n    cfg << c << \"\\n\";\n    cfg.close();\n  }\n\n  clog_redirect::set_enabled(true);\n\n  auto tt_hash = std::pair{\"timetable\"s, cista::BASE_HASH};\n  if (c.timetable_.has_value()) {\n    auto& h = tt_hash.second;\n    auto const& t = *c.timetable_;\n\n    for (auto const& [_, d] : t.datasets_) {\n      h = cista::build_seeded_hash(\n          h, c.osr_footpath_, hash_file(d.path_), d.default_bikes_allowed_,\n          d.default_cars_allowed_, d.clasz_bikes_allowed_,\n          d.clasz_cars_allowed_, d.default_timezone_, d.extend_calendar_);\n      if (d.script_.has_value()) {\n        h = cista::build_seeded_hash(h, hash_file(*d.script_));\n      }\n    }\n\n    h = cista::build_seeded_hash(\n        h, t.first_day_, t.num_days_, t.with_shapes_, t.adjust_footpaths_,\n        t.merge_dupes_intra_src_, t.merge_dupes_inter_src_,\n        t.link_stop_distance_, t.update_interval_, t.incremental_rt_update_,\n        t.max_footpath_length_, t.default_timezone_, t.assistance_times_);\n  }\n\n  auto osm_hash = std::pair{\"osm\"s, cista::BASE_HASH};\n  if (c.osm_.has_value()) {\n    osm_hash.second = hash_file(*c.osm_);\n  }\n\n  auto const elevation_dir =\n      c.get_street_routing()\n          .and_then([](config::street_routing const& sr) {\n            return sr.elevation_data_dir_;\n          })\n          .value_or(fs::path{});\n  auto elevation_dir_hash = std::pair{\"elevation_dir\"s, cista::BASE_HASH};\n  if (!elevation_dir.empty() && fs::exists(elevation_dir)) {\n    auto files = std::vector<std::string>{};\n    for (auto const& f : fs::recursive_directory_iterator(elevation_dir)) {\n      if (f.is_regular_file()) {\n        files.emplace_back(f.path().relative_path().string());\n      }\n    }\n    std::ranges::sort(files);\n    auto& h = elevation_dir_hash.second;\n    for (auto const& f : files) {\n      h = cista::build_seeded_hash(h, f);\n    }\n  }\n\n  auto tiles_hash = std::pair{\"tiles_profile\", cista::BASE_HASH};\n  if (c.tiles_.has_value()) {\n    auto& h = tiles_hash.second;\n    h = cista::build_hash(hash_file(c.tiles_->profile_), c.tiles_->db_size_);\n    if (c.tiles_->coastline_.has_value()) {\n      h = cista::hash_combine(h, hash_file(*c.tiles_->coastline_));\n    }\n  }\n\n  auto const to_clasz_bool_array =\n      [&](bool const default_allowed,\n          std::optional<std::map<std::string, bool>> const& clasz_allowed) {\n        auto a = std::array<bool, n::kNumClasses>{};\n        a.fill(default_allowed);\n        if (clasz_allowed.has_value()) {\n          for (auto const& [clasz, allowed] : *clasz_allowed) {\n            a[static_cast<unsigned>(n::to_clasz(clasz))] = allowed;\n          }\n        }\n        return a;\n      };\n\n  auto const route_shapes_clasz_enabled = to_clasz_bool_array(\n      true, c.timetable_.value_or(config::timetable{})\n                .route_shapes_.value_or(config::timetable::route_shapes{})\n                .clasz_);\n  auto route_shapes_clasz_hash =\n      std::pair{\"route_shapes_clasz\"s, cista::BASE_HASH};\n  for (auto const& b : route_shapes_clasz_enabled) {\n    route_shapes_clasz_hash.second =\n        cista::build_seeded_hash(route_shapes_clasz_hash.second, b);\n  }\n\n  auto const shape_cache_path = data_path / \"routed_shapes_cache.mdb\";\n  auto const shape_cache_lock_path =\n      fs::path{shape_cache_path.generic_string() + \"-lock\"};\n  auto const route_shapes_task_enabled =\n      c.timetable_\n          .transform([](auto&& x) { return x.route_shapes_.has_value(); })\n          .value_or(false);\n  auto const existing_rs_hashes = read_hashes(data_path, \"route_shapes\");\n  auto const route_shapes_reuse_old_osm_data =\n      c.timetable_.value_or(config::timetable{})\n          .route_shapes_.value_or(config::timetable::route_shapes{})\n          .cache_reuse_old_osm_data_;\n  auto const reuse_shapes_cache =\n      // cache must exist (handles case where files were deleted manually)\n      fs::exists(shape_cache_path) &&\n      // and have the same routed_shapes_ver\n      (existing_rs_hashes.find(\"routed_shapes_ver\") !=\n           end(existing_rs_hashes) &&\n       existing_rs_hashes.at(\"routed_shapes_ver\") ==\n           routed_shapes_version().second) &&\n      // if route_shapes_reuse_old_osm_data, we can reuse any data\n      // otherwise only if the osm data is the same\n      (route_shapes_reuse_old_osm_data ||\n       (existing_rs_hashes.find(osm_hash.first) != end(existing_rs_hashes) &&\n        existing_rs_hashes.at(osm_hash.first) == osm_hash.second &&\n        // cache_reuse_old_osm_data flag must be the same or changed from 0->1\n        // otherwise cache may contain old data from previous runs\n        existing_rs_hashes.find(\"cache_reuse_old_osm_data\") !=\n            end(existing_rs_hashes) &&\n        (existing_rs_hashes.at(\"cache_reuse_old_osm_data\") ==\n             static_cast<std::uint64_t>(route_shapes_reuse_old_osm_data) ||\n         existing_rs_hashes.at(\"cache_reuse_old_osm_data\") == 0)));\n\n  auto const keep_routed_shape_data =\n      !route_shapes_task_enabled || reuse_shapes_cache;\n\n  if (!keep_routed_shape_data) {\n    fs::remove(shape_cache_path, ec);\n    fs::remove(shape_cache_lock_path, ec);\n  }\n\n  auto osr = task{\"osr\",\n                  {},\n                  c.use_street_routing(),\n                  [&]() {\n                    osr::extract(true, fs::path{*c.osm_}, data_path / \"osr\",\n                                 elevation_dir);\n                  },\n                  {osm_hash, osr_version(), elevation_dir_hash}};\n\n  auto adr = task{\"adr\",\n                  {},\n                  c.geocoding_ || c.reverse_geocoding_,\n                  [&]() {\n                    if (!c.osm_) {\n                      return;\n                    }\n                    adr::extract(*c.osm_, data_path / \"adr\", data_path / \"adr\");\n                  },\n                  {osm_hash,\n                   adr_version(),\n                   {\"geocoding\", c.geocoding_},\n                   {\"reverse_geocoding\", c.reverse_geocoding_}}};\n\n  auto tt = task{\n      \"tt\",\n      {},\n      c.timetable_.has_value(),\n      [&]() {\n        auto const& t = *c.timetable_;\n\n        auto const first_day =\n            n::parse_date(t.first_day_) - std::chrono::days{1};\n        auto const interval = n::interval<date::sys_days>{\n            first_day, first_day + std::chrono::days{t.num_days_ + 1U}};\n\n        auto assistance = std::unique_ptr<nl::assistance_times>{};\n        if (t.assistance_times_.has_value()) {\n          auto const f =\n              cista::mmap{t.assistance_times_->generic_string().c_str(),\n                          cista::mmap::protection::READ};\n          assistance = std::make_unique<nl::assistance_times>(\n              nl::read_assistance(f.view()));\n        }\n\n        auto shapes = std::unique_ptr<n::shapes_storage>{};\n        if (t.with_shapes_) {\n          shapes = std::make_unique<n::shapes_storage>(\n              data_path, cista::mmap::protection::WRITE,\n              keep_routed_shape_data);\n        }\n\n        auto tags = cista::wrapped{cista::raw::make_unique<tag_lookup>()};\n        auto tt = cista::wrapped{cista::raw::make_unique<n::timetable>(nl::load(\n            utl::to_vec(\n                t.datasets_,\n                [&, src = n::source_idx_t{}](\n                    std::pair<std::string, config::timetable::dataset> const&\n                        x) mutable -> nl::timetable_source {\n                  auto const& [tag, dc] = x;\n                  tags->add(src++, tag);\n                  return {\n                      tag,\n                      dc.path_,\n                      {.link_stop_distance_ = t.link_stop_distance_,\n                       .default_tz_ = dc.default_timezone_.value_or(\n                           t.default_timezone_.value_or(\"\")),\n                       .bikes_allowed_default_ = to_clasz_bool_array(\n                           dc.default_bikes_allowed_, dc.clasz_bikes_allowed_),\n                       .cars_allowed_default_ = to_clasz_bool_array(\n                           dc.default_cars_allowed_, dc.clasz_cars_allowed_),\n                       .extend_calendar_ = dc.extend_calendar_,\n                       .user_script_ =\n                           dc.script_\n                               .and_then([](std::string const& path) {\n                                 if (path.starts_with(\"\\nfunction\")) {\n                                   return std::optional{path};\n                                 }\n                                 return std::optional{std::string{\n                                     cista::mmap{path.c_str(),\n                                                 cista::mmap::protection::READ}\n                                         .view()}};\n                               })\n                               .value_or(\"\")}};\n                }),\n            {.adjust_footpaths_ = t.adjust_footpaths_,\n             .merge_dupes_intra_src_ = t.merge_dupes_intra_src_,\n             .merge_dupes_inter_src_ = t.merge_dupes_inter_src_,\n             .max_footpath_length_ = t.max_footpath_length_},\n            interval, assistance.get(), shapes.get(), false))};\n\n        tt->write(data_path / \"tt.bin\");\n        tags->write(data_path / \"tags.bin\");\n        std::ofstream(data_path / \"timetable_metrics.json\")\n            << to_str(n::get_metrics(*tt), *tt);\n      },\n      {tt_hash, n_version()}};\n\n  auto tbd = task{\"tbd\",\n                  {&tt},\n                  c.timetable_.has_value() && c.timetable_->tb_,\n                  [&]() {\n                    auto d = data{data_path};\n                    d.load_tt(\"tt.bin\");\n                    cista::write(\n                        data_path / \"tbd.bin\",\n                        n::routing::tb::preprocess(*d.tt_, n::kDefaultProfile));\n                  },\n                  {tt_hash, n_version(), tbd_version()}};\n\n  auto adr_extend = task{\n      \"adr_extend\",\n      c.osm_.has_value() ? std::vector<task*>{&adr, &tt}\n                         : std::vector<task*>{&tt},\n      c.timetable_.has_value() && (c.geocoding_ || c.reverse_geocoding_),\n      [&]() {\n        auto d = data{data_path};\n        d.load_tt(\"tt.bin\");\n        if (c.osm_) {\n          d.t_ = adr::read(data_path / \"adr\" / \"t.bin\");\n          d.tc_ = std::make_unique<adr::cache>(d.t_->strings_.size(), 100U);\n          d.f_ = std::make_unique<adr::formatter>();\n        }\n\n        auto const area_db = d.t_ ? (std::optional<adr::area_database>{\n                                        std::in_place, data_path / \"adr\",\n                                        cista::mmap::protection::READ})\n                                  : std::nullopt;\n        if (!d.t_) {\n          d.t_ = cista::wrapped<adr::typeahead>{\n              cista::raw::make_unique<adr::typeahead>()};\n        }\n        auto const location_extra_place = adr_extend_tt(\n            *d.tt_, area_db.has_value() ? &*area_db : nullptr, *d.t_);\n        auto ec = std::error_code{};\n        std::filesystem::create_directories(data_path / \"adr\", ec);\n        cista::write(data_path / \"adr\" / \"t_ext.bin\", *d.t_);\n        cista::write(data_path / \"adr\" / \"location_extra_place.bin\",\n                     location_extra_place);\n        {\n          auto r =\n              adr::reverse{data_path / \"adr\", cista::mmap::protection::WRITE};\n          r.build_rtree(*d.t_);\n          r.write();\n        }\n\n        cista::free_self_allocated(d.t_.get());\n      },\n      {tt_hash,\n       osm_hash,\n       adr_version(),\n       adr_ext_version(),\n       n_version(),\n       {\"geocoding\", c.geocoding_},\n       {\"reverse_geocoding\", c.reverse_geocoding_}}};\n\n  auto osr_footpath_settings_hash =\n      meta_entry_t{\"osr_footpath_settings\", cista::BASE_HASH};\n  if (c.timetable_) {\n    auto& h = osr_footpath_settings_hash.second;\n    h = cista::hash_combine(h, c.timetable_->use_osm_stop_coordinates_,\n                            c.timetable_->extend_missing_footpaths_,\n                            c.timetable_->max_matching_distance_,\n                            c.timetable_->max_footpath_length_);\n  }\n  auto osr_footpath = task{\n      \"osr_footpath\",\n      {&tt, &osr},\n      c.osr_footpath_ && c.timetable_,\n      [&]() {\n        auto d = data{data_path};\n        d.load_tt(\"tt.bin\");\n        d.load_osr();\n\n        auto const profiles = std::vector<routed_transfers_settings>{\n            {.profile_ = osr::search_profile::kFoot,\n             .profile_idx_ = n::kFootProfile,\n             .max_matching_distance_ = c.timetable_->max_matching_distance_,\n             .extend_missing_ = c.timetable_->extend_missing_footpaths_,\n             .max_duration_ = c.timetable_->max_footpath_length_ * 1min},\n            {.profile_ = osr::search_profile::kWheelchair,\n             .profile_idx_ = n::kWheelchairProfile,\n             .max_matching_distance_ = 8.0,\n             .max_duration_ = c.timetable_->max_footpath_length_ * 1min},\n            {.profile_ = osr::search_profile::kCar,\n             .profile_idx_ = n::kCarProfile,\n             .max_matching_distance_ = 250.0,\n             .max_duration_ = 8h,\n             .is_candidate_ = [&](n::location_idx_t const l) {\n               return utl::any_of(d.tt_->location_routes_[l], [&](auto r) {\n                 return d.tt_->has_car_transport(r);\n               });\n             }}};\n        auto const elevator_footpath_map = compute_footpaths(\n            *d.w_, *d.l_, *d.pl_, *d.tt_, d.elevations_.get(),\n            c.timetable_->use_osm_stop_coordinates_, profiles);\n\n        cista::write(data_path / \"elevator_footpath_map.bin\",\n                     elevator_footpath_map);\n        d.tt_->write(data_path / \"tt_ext.bin\");\n\n        cista::free_self_allocated(d.tt_.get());\n      },\n      {tt_hash, osm_hash, osr_footpath_settings_hash, osr_version(),\n       osr_footpath_version(), n_version()}};\n\n  auto matches = task{\n      \"matches\",\n      {&tt, &osr, &osr_footpath},\n      c.timetable_ && c.use_street_routing(),\n      [&]() {\n        auto d = data{data_path};\n        d.load_tt(c.osr_footpath_ ? \"tt_ext.bin\" : \"tt.bin\");\n        d.load_osr();\n\n        auto const progress_tracker = utl::get_active_progress_tracker();\n        progress_tracker->status(\"Prepare Platform Matches\").out_bounds(0, 30);\n        cista::write(data_path / \"matches.bin\",\n                     get_matches(*d.tt_, *d.pl_, *d.w_));\n\n        d.load_matches();\n        if (c.timetable_.value().preprocess_max_matching_distance_ > 0.0) {\n          progress_tracker->status(\"Prepare Platform Way Matches\")\n              .out_bounds(30, 100);\n          way_matches_storage{\n              data_path, cista::mmap::protection::WRITE,\n              c.timetable_.value().preprocess_max_matching_distance_}\n              .preprocess_osr_matches(*d.tt_, *d.pl_, *d.w_, *d.l_,\n                                      *d.matches_);\n        }\n      },\n      {tt_hash, osm_hash, osr_version(), n_version(), matches_version(),\n       std::pair{\"way_matches\",\n                 cista::build_hash(c.timetable_.value_or(config::timetable{})\n                                       .preprocess_max_matching_distance_)}}};\n\n  auto route_shapes_task = task{\n      \"route_shapes\",\n      {&tt, &osr},\n      route_shapes_task_enabled,\n      [&]() {\n        auto d = data{data_path};\n        d.load_tt(\"tt.bin\");\n        d.load_osr();\n\n        auto shape_cache = std::unique_ptr<motis::shape_cache>{};\n        if (reuse_shapes_cache) {\n          std::clog << \"loading existing shape cache from \" << shape_cache_path\n                    << \"\\n\";\n        } else {\n          std::clog << \"creating new shape cache\\n\";\n        }\n        shape_cache = std::make_unique<motis::shape_cache>(\n            shape_cache_path, c.timetable_->route_shapes_->cache_db_size_);\n\n        // re-open in write mode\n        // this needs to be done in two steps, because the files need to be\n        // closed first, before they can be re-opened in write mode (at\n        // least on Windows)\n        d.shapes_ = {};\n        auto shapes = n::shapes_storage{\n            data_path, cista::mmap::protection::MODIFY, reuse_shapes_cache};\n        route_shapes(*d.w_, *d.l_, *d.tt_, shapes, *c.timetable_->route_shapes_,\n                     route_shapes_clasz_enabled, shape_cache.get());\n      },\n      {tt_hash,\n       osm_hash,\n       osr_version(),\n       n_version(),\n       routed_shapes_version(),\n       route_shapes_clasz_hash,\n       {\"route_shapes_mode\",\n        static_cast<std::uint64_t>(\n            c.timetable_.value_or(config::timetable{})\n                .route_shapes_.value_or(config::timetable::route_shapes{})\n                .mode_)},\n       {\"cache_reuse_old_osm_data\",\n        c.timetable_.value_or(config::timetable{})\n            .route_shapes_.value_or(config::timetable::route_shapes{})\n            .cache_reuse_old_osm_data_}}};\n\n  auto tiles = task{\n      \"tiles\",\n      {},\n      c.tiles_.has_value(),\n      [&]() {\n        auto const progress_tracker = utl::get_active_progress_tracker();\n\n        auto const dir = data_path / \"tiles\";\n        auto const path = (dir / \"tiles.mdb\").string();\n\n        auto ec = std::error_code{};\n        fs::create_directories(data_path / \"tiles\", ec);\n\n        progress_tracker->status(\"Clear Database\");\n        ::tiles::clear_database(path, c.tiles_->db_size_);\n        ::tiles::clear_pack_file(path.c_str());\n\n        auto db_env =\n            ::tiles::make_tile_database(path.c_str(), c.tiles_->db_size_);\n        ::tiles::tile_db_handle db_handle{db_env};\n        ::tiles::pack_handle pack_handle{path.c_str()};\n\n        {\n          ::tiles::feature_inserter_mt inserter{\n              ::tiles::dbi_handle{db_handle, db_handle.features_dbi_opener()},\n              pack_handle};\n\n          if (c.tiles_->coastline_.has_value()) {\n            progress_tracker->status(\"Load Coastlines\").out_bounds(0, 20);\n            ::tiles::load_coastlines(db_handle, inserter,\n                                     c.tiles_->coastline_->generic_string());\n          }\n\n          progress_tracker->status(\"Load Features\").out_bounds(20, 70);\n          ::tiles::load_osm(db_handle, inserter, c.osm_->generic_string(),\n                            c.tiles_->profile_.generic_string(),\n                            dir.generic_string(), c.tiles_->flush_threshold_);\n        }\n\n        progress_tracker->status(\"Pack Features\").out_bounds(70, 90);\n        ::tiles::pack_features(db_handle, pack_handle);\n\n        progress_tracker->status(\"Prepare Tiles\").out_bounds(90, 100);\n        ::tiles::prepare_tiles(db_handle, pack_handle, 10);\n      },\n      {tiles_version(), osm_hash, tiles_hash}};\n\n  auto all_tasks = std::vector{&tiles,        &osr,     &adr,\n                               &tt,           &tbd,     &adr_extend,\n                               &osr_footpath, &matches, &route_shapes_task};\n  auto todo = std::set<task*>{};\n  if (task_filter.has_value()) {\n    auto q = std::vector<task*>{};\n    for (auto const& x : *task_filter) {\n      auto const it =\n          utl::find_if(all_tasks, [&](task* t) { return t->name_ == x; });\n      utl::verify(it != end(all_tasks) && (*it)->should_run_,\n                  \"task {} not found or disabled\", x);\n      q.push_back(*it);\n    }\n\n    while (!q.empty()) {\n      auto const next = q.back();\n      q.resize(q.size() - 1);\n      todo.insert(next);\n      for (auto const& x : next->dependencies_) {\n        if (!todo.contains(x)) {\n          q.push_back(x);\n        }\n      }\n    }\n  } else {\n    todo.insert(begin(all_tasks), end(all_tasks));\n  }\n\n  auto tasks = std::vector<task*>{begin(todo), end(todo)};\n  utl::erase_if(tasks, [&](task* t) {\n    if (!t->should_run_ || t->ready_for_load(data_path)) {\n      t->done_ = true;\n      return true;\n    }\n    return false;\n  });\n\n  fmt::println(\"running tasks: {}\",\n               tasks | std::views::transform([](task* t) { return *t; }));\n  for (auto* t : tasks) {\n    t->pt_ = utl::activate_progress_tracker(t->name_);\n  }\n\n  while (!tasks.empty()) {\n    auto const task_it = utl::find_if(tasks, [](task const* t) {\n      return utl::all_of(t->dependencies_,\n                         [](task const* t) { return t->done_; });\n    });\n    utl::verify(\n        task_it != end(tasks), \"no task to run, remaining tasks: {}\",\n        tasks | std::views::transform([](task const* t) { return *t; }));\n    (*task_it)->run(data_path);\n    tasks.erase(task_it);\n  }\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/journey_to_response.cc",
    "content": "#include \"motis/journey_to_response.h\"\n\n#include <cmath>\n#include <iostream>\n#include <span>\n#include <variant>\n\n#include \"utl/enumerate.h\"\n#include \"utl/overloaded.h\"\n#include \"utl/visit.h\"\n\n#include \"geo/polyline_format.h\"\n\n#include \"nigiri/common/split_duration.h\"\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/gtfsrt_resolve_run.h\"\n#include \"nigiri/rt/service_alert.h\"\n#include \"nigiri/special_stations.h\"\n#include \"nigiri/types.h\"\n\n#include \"adr/typeahead.h\"\n#include \"motis-api/motis-api.h\"\n#include \"motis/constants.h\"\n#include \"motis/flex/flex_output.h\"\n#include \"motis/gbfs/gbfs_output.h\"\n#include \"motis/gbfs/routing_data.h\"\n#include \"motis/odm/odm.h\"\n#include \"motis/osr/mode_to_profile.h\"\n#include \"motis/osr/street_routing.h\"\n#include \"motis/place.h\"\n#include \"motis/polyline.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/clasz_to_mode.h\"\n#include \"motis/timetable/time_conv.h\"\n#include \"motis/transport_mode_ids.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\napi::ModeEnum to_mode(osr::search_profile const m) {\n  switch (m) {\n    case osr::search_profile::kCarParkingWheelchair: [[fallthrough]];\n    case osr::search_profile::kCarParking: return api::ModeEnum::CAR_PARKING;\n    case osr::search_profile::kCarDropOffWheelchair: [[fallthrough]];\n    case osr::search_profile::kCarDropOff: return api::ModeEnum::CAR_DROPOFF;\n    case osr::search_profile::kFoot: [[fallthrough]];\n    case osr::search_profile::kWheelchair: return api::ModeEnum::WALK;\n    case osr::search_profile::kCar: return api::ModeEnum::CAR;\n    case osr::search_profile::kBikeElevationLow: [[fallthrough]];\n    case osr::search_profile::kBikeElevationHigh: [[fallthrough]];\n    case osr::search_profile::kBikeFast: [[fallthrough]];\n    case osr::search_profile::kBike: return api::ModeEnum::BIKE;\n    case osr::search_profile::kBikeSharing: [[fallthrough]];\n    case osr::search_profile::kCarSharing: return api::ModeEnum::RENTAL;\n    case osr::search_profile::kBus: return api::ModeEnum::DEBUG_BUS_ROUTE;\n    case osr::search_profile::kRailway:\n      return api::ModeEnum::DEBUG_RAILWAY_ROUTE;\n    case osr::search_profile::kFerry: return api::ModeEnum::DEBUG_FERRY_ROUTE;\n  }\n  std::unreachable();\n}\n\nvoid cleanup_intermodal(api::Itinerary& i) {\n  if (i.legs_.front().from_.name_ == \"END\") {\n    i.legs_.front().from_.name_ = \"START\";\n  }\n  if (i.legs_.back().to_.name_ == \"START\") {\n    i.legs_.back().to_.name_ = \"END\";\n  }\n}\n\nstruct fare_indices {\n  std::int64_t transfer_idx_;\n  std::int64_t effective_fare_leg_idx_;\n};\n\nstd::optional<fare_indices> get_fare_indices(\n    std::optional<std::vector<n::fare_transfer>> const& fares,\n    n::routing::journey::leg const& l) {\n  if (!fares.has_value()) {\n    return std::nullopt;\n  }\n\n  for (auto const [transfer_idx, transfer] : utl::enumerate(*fares)) {\n    for (auto const [eff_fare_leg_idx, eff_fare_leg] :\n         utl::enumerate(transfer.legs_)) {\n      for (auto const* x : eff_fare_leg.joined_leg_) {\n        if (x == &l) {\n          return fare_indices{static_cast<std::int64_t>(transfer_idx),\n                              static_cast<std::int64_t>(eff_fare_leg_idx)};\n        }\n      }\n    }\n  }\n\n  return std::nullopt;\n}\n\nstd::optional<std::vector<api::Alert>> get_alerts(\n    n::rt::frun const& fr,\n    std::optional<std::pair<n::rt::run_stop, n::event_type>> const& s,\n    bool const fuzzy_stop,\n    std::optional<std::vector<std::string>> const& language) {\n  if (fr.rtt_ == nullptr || !fr.is_scheduled()) {  // TODO added\n    return std::nullopt;\n  }\n\n  auto const& tt = *fr.tt_;\n  auto const* rtt = fr.rtt_;\n\n  auto const to_time_range =\n      [&](n::interval<n::unixtime_t> const x) -> api::TimeRange {\n    return {x.from_, x.to_};\n  };\n  auto const to_cause = [](n::alert_cause const x) {\n    return api::AlertCauseEnum{static_cast<int>(x)};\n  };\n  auto const to_effect = [](n::alert_effect const x) {\n    return api::AlertEffectEnum{static_cast<int>(x)};\n  };\n  auto const convert_to_str = [](std::string_view s) {\n    return std::optional{std::string{s}};\n  };\n  auto const to_alert = [&](n::alert_idx_t const x) -> api::Alert {\n    auto const& a = rtt->alerts_;\n    auto const get_translation =\n        [&](auto const& translations) -> std::optional<std::string> {\n      if (translations.empty()) {\n        return std::nullopt;\n      } else if (!language.has_value()) {\n        return a.strings_.try_get(translations.front().text_)\n            .and_then(convert_to_str);\n      } else {\n        for (auto const& req_lang : *language) {\n          auto const it = utl::find_if(\n              translations, [&](n::alert_translation const translation) {\n                auto const translation_lang =\n                    a.strings_.try_get(translation.language_);\n                return translation_lang.has_value() &&\n                       translation_lang->starts_with(req_lang);\n              });\n          if (it == end(translations)) {\n            continue;\n          }\n          return a.strings_.try_get(it->text_).and_then(convert_to_str);\n        }\n        return a.strings_.try_get(translations.front().text_)\n            .and_then(convert_to_str);\n      }\n    };\n    return {\n        .communicationPeriod_ =\n            a.communication_period_[x].empty()\n                ? std::nullopt\n                : std::optional{utl::to_vec(a.communication_period_[x],\n                                            to_time_range)},\n        .impactPeriod_ = a.impact_period_[x].empty()\n                             ? std::nullopt\n                             : std::optional{utl::to_vec(a.impact_period_[x],\n                                                         to_time_range)},\n        .cause_ = to_cause(a.cause_[x]),\n        .causeDetail_ = get_translation(a.cause_detail_[x]),\n        .effect_ = to_effect(a.effect_[x]),\n        .effectDetail_ = get_translation(a.effect_detail_[x]),\n        .url_ = get_translation(a.url_[x]),\n        .headerText_ = get_translation(a.header_text_[x]).value_or(\"\"),\n        .descriptionText_ =\n            get_translation(a.description_text_[x]).value_or(\"\"),\n        .ttsHeaderText_ = get_translation(a.tts_header_text_[x]),\n        .ttsDescriptionText_ = get_translation(a.tts_description_text_[x]),\n        .imageUrl_ = a.image_[x].empty()\n                         ? std::nullopt\n                         : a.strings_.try_get(a.image_[x].front().url_)\n                               .and_then(convert_to_str),\n        .imageMediaType_ =\n            a.image_[x].empty()\n                ? std::nullopt\n                : a.strings_.try_get(a.image_[x].front().media_type_)\n                      .and_then(convert_to_str),\n        .imageAlternativeText_ = get_translation(a.image_alternative_text_[x])};\n  };\n\n  auto const x =\n      s.and_then([](std::pair<n::rt::run_stop, n::event_type> const& rs_ev) {\n         auto const& [rs, ev_type] = rs_ev;\n         return std::optional{rs.get_trip_idx(ev_type)};\n       }).value_or(fr.trip_idx());\n  auto const l =\n      s.and_then([](std::pair<n::rt::run_stop, n::event_type> const& rs) {\n         return std::optional{rs.first.get_location_idx()};\n       }).value_or(n::location_idx_t::invalid());\n  auto alerts = std::vector<api::Alert>{};\n  for (auto const& t : tt.trip_ids_[x]) {\n    auto const src = tt.trip_id_src_[t];\n    for (auto const& a :\n         rtt->alerts_.get_alerts(tt, src, x, fr.rt_, l, fuzzy_stop)) {\n      alerts.emplace_back(to_alert(a));\n    }\n  }\n\n  return alerts.empty() ? std::nullopt : std::optional{std::move(alerts)};\n}\n\nstruct parent_name_hash {\n  bool operator()(n::location_idx_t const l) const {\n    return cista::hash(tt_->get_default_name(tt_->locations_.get_root_idx(l)));\n  }\n  n::timetable const* tt_{nullptr};\n};\n\nstruct parent_name_eq {\n  bool operator()(n::location_idx_t const a, n::location_idx_t const b) const {\n    return tt_->get_default_name(tt_->locations_.get_root_idx(a)) ==\n           tt_->get_default_name(tt_->locations_.get_root_idx(b));\n  }\n  n::timetable const* tt_{nullptr};\n};\n\nusing unique_stop_map_t =\n    hash_map<n::location_idx_t, bool, parent_name_hash, parent_name_eq>;\n\nvoid get_is_unique_stop_name(n::rt::frun const& fr,\n                             n::interval<n::stop_idx_t> const& stops,\n                             unique_stop_map_t& is_unique) {\n  is_unique.clear();\n  for (auto const i : stops) {\n    auto const [it, is_new] = is_unique.emplace(fr[i].get_location_idx(), true);\n    if (!is_new) {\n      it->second = false;\n    }\n  }\n}\n\napi::Itinerary journey_to_response(\n    osr::ways const* w,\n    osr::lookup const* l,\n    osr::platforms const* pl,\n    n::timetable const& tt,\n    tag_lookup const& tags,\n    flex::flex_areas const* fl,\n    elevators const* e,\n    n::rt_timetable const* rtt,\n    platform_matches_t const* matches,\n    osr::elevation_storage const* elevations,\n    n::shapes_storage const* shapes,\n    gbfs::gbfs_routing_data& gbfs_rd,\n    adr_ext const* ae,\n    tz_map_t const* tz_map,\n    n::routing::journey const& j,\n    place_t const& start,\n    place_t const& dest,\n    street_routing_cache_t& cache,\n    osr::bitvec<osr::node_idx_t>* blocked_mem,\n    bool const car_transfers,\n    osr_parameters const& osr_params,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs,\n    bool const join_interlined_legs,\n    bool const detailed_transfers,\n    bool const detailed_legs,\n    bool const with_fares,\n    bool const with_scheduled_skipped_stops,\n    double const timetable_max_matching_distance,\n    double const max_matching_distance,\n    unsigned const api_version,\n    bool const ignore_start_rental_return_constraints,\n    bool const ignore_dest_rental_return_constraints,\n    n::lang_t const& lang) {\n  utl::verify(!j.legs_.empty(), \"journey without legs\");\n\n  auto const fares =\n      with_fares ? std::optional{n::get_fares(tt, rtt, j)} : std::nullopt;\n  auto const to_fare_media_type =\n      [](n::fares::fare_media::fare_media_type const t) {\n        using fare_media_type = n::fares::fare_media::fare_media_type;\n        switch (t) {\n          case fare_media_type::kNone: return api::FareMediaTypeEnum::NONE;\n          case fare_media_type::kPaper:\n            return api::FareMediaTypeEnum::PAPER_TICKET;\n          case fare_media_type::kCard:\n            return api::FareMediaTypeEnum::TRANSIT_CARD;\n          case fare_media_type::kContactless:\n            return api::FareMediaTypeEnum::CONTACTLESS_EMV;\n          case fare_media_type::kApp: return api::FareMediaTypeEnum::MOBILE_APP;\n        }\n        std::unreachable();\n      };\n  auto const to_media = [&](n::fares::fare_media const& m) -> api::FareMedia {\n    return {.fareMediaName_ =\n                m.name_ == n::string_idx_t::invalid()\n                    ? std::nullopt\n                    : std::optional{std::string{tt.strings_.get(m.name_)}},\n            .fareMediaType_ = to_fare_media_type(m.type_)};\n  };\n  auto const to_rider_category =\n      [&](n::fares::rider_category const& r) -> api::RiderCategory {\n    return {.riderCategoryName_ = std::string{tt.strings_.get(r.name_)},\n            .isDefaultFareCategory_ = r.is_default_fare_category_,\n            .eligibilityUrl_ = tt.strings_.try_get(r.eligibility_url_)\n                                   .and_then([](std::string_view s) {\n                                     return std::optional{std::string{s}};\n                                   })};\n  };\n  auto const to_products =\n      [&](n::fares const& f,\n          n::fare_product_idx_t const x) -> std::vector<api::FareProduct> {\n    if (x == n::fare_product_idx_t::invalid()) {\n      return {};\n    }\n    return utl::to_vec(\n        f.fare_products_[x],\n        [&](n::fares::fare_product const& p) -> api::FareProduct {\n          return {\n              .name_ = std::string{tt.strings_.get(p.name_)},\n              .amount_ = p.amount_,\n              .currency_ = std::string{tt.strings_.get(p.currency_code_)},\n              .riderCategory_ =\n                  p.rider_category_ == n::rider_category_idx_t::invalid()\n                      ? std::nullopt\n                      : std::optional{to_rider_category(\n                            f.rider_categories_[p.rider_category_])},\n              .media_ = p.media_ == n::fare_media_idx_t::invalid()\n                            ? std::nullopt\n                            : std::optional{to_media(f.fare_media_[p.media_])}};\n        });\n  };\n  auto const to_rule = [](n::fares::fare_transfer_rule const& x) {\n    switch (x.fare_transfer_type_) {\n      case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAB:\n        return api::FareTransferRuleEnum::AB;\n      case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAPlusAB:\n        return api::FareTransferRuleEnum::A_AB;\n      case nigiri::fares::fare_transfer_rule::fare_transfer_type::kAPlusABPlusB:\n        return api::FareTransferRuleEnum::A_AB_B;\n    }\n    std::unreachable();\n  };\n\n  auto itinerary = api::Itinerary{\n      .duration_ = to_seconds(j.arrival_time() - j.departure_time()),\n      .startTime_ = j.legs_.front().dep_time_,\n      .endTime_ = j.legs_.back().arr_time_,\n      .transfers_ = std::max(\n          static_cast<std::iterator_traits<\n              decltype(j.legs_)::iterator>::difference_type>(0),\n          utl::count_if(\n              j.legs_,\n              [](n::routing::journey::leg const& leg) {\n                return holds_alternative<n::routing::journey::run_enter_exit>(\n                           leg.uses_) ||\n                       odm::is_odm_leg(leg, kOdmTransportModeId) ||\n                       odm::is_odm_leg(leg, kRideSharingTransportModeId);\n              }) -\n              1),\n      .fareTransfers_ =\n          fares.and_then([&](std::vector<n::fare_transfer> const& transfers) {\n            return std::optional{utl::to_vec(\n                transfers, [&](n::fare_transfer const& t) -> api::FareTransfer {\n                  return {.rule_ = t.rule_.and_then([&](auto&& r) {\n                            return std::optional{to_rule(r)};\n                          }),\n                          .transferProducts_ = t.rule_.and_then([&](auto&& r) {\n                            return t.legs_.empty()\n                                       ? std::nullopt\n                                       : std::optional{to_products(\n                                             tt.fares_[t.legs_.front().src_],\n                                             r.fare_product_)};\n                          }),\n                          .effectiveFareLegProducts_ =\n                              utl::to_vec(t.legs_, [&](auto&& l) {\n                                return utl::to_vec(l.rule_, [&](auto&& r) {\n                                  return to_products(tt.fares_[l.src_],\n                                                     r.fare_product_);\n                                });\n                              })};\n                })};\n          })};\n\n  auto const append = [&](api::Itinerary&& x) {\n    itinerary.legs_.insert(end(itinerary.legs_),\n                           std::move_iterator{begin(x.legs_)},\n                           std::move_iterator{end(x.legs_)});\n  };\n\n  auto const get_first_run_tz = [&]() -> std::optional<std::string> {\n    if (j.legs_.size() < 2) {\n      return std::nullopt;\n    }\n    auto const osm_tz = get_tz(tt, ae, tz_map, j.legs_[1].from_);\n    if (osm_tz != nullptr) {\n      return std::optional{osm_tz->name()};\n    }\n    return utl::visit(\n        j.legs_[1].uses_, [&](n::routing::journey::run_enter_exit const& x) {\n          return n::rt::frun{tt, rtt, x.r_}[0].get_tz_name(n::event_type::kDep);\n        });\n  };\n\n  for (auto const [j_leg_idx, j_leg] : utl::enumerate(j.legs_)) {\n    auto const pred =\n        itinerary.legs_.empty() ? nullptr : &itinerary.legs_.back();\n    auto const fallback_tz =\n        pred == nullptr ? get_first_run_tz() : pred->to_.tz_;\n    auto const from =\n        pred == nullptr\n            ? to_place(&tt, &tags, w, pl, matches, ae, tz_map, lang,\n                       tt_location{j_leg.from_}, start, dest, \"\", fallback_tz)\n            : pred->to_;\n    auto const to =\n        to_place(&tt, &tags, w, pl, matches, ae, tz_map, lang,\n                 tt_location{j_leg.to_}, start, dest, \"\", fallback_tz);\n\n    auto is_unique =\n        unique_stop_map_t{0U, parent_name_hash{&tt}, parent_name_eq{&tt}};\n    auto const to_place = [&](n::rt::run_stop const& s,\n                              n::event_type const ev_type) {\n      auto p = ::motis::to_place(&tt, &tags, w, pl, matches, ae, tz_map, lang,\n                                 s, start, dest);\n      p.alerts_ = get_alerts(*s.fr_, std::pair{s, ev_type}, false, lang);\n      if (auto const it = is_unique.find(s.get_location_idx());\n          it != end(is_unique) && !it->second) {\n        p.name_ =\n            tt.translate(lang, tt.locations_.names_[s.get_location_idx()]);\n      }\n      return p;\n    };\n\n    std::visit(\n        utl::overloaded{\n            [&](n::routing::journey::run_enter_exit const& t) {\n              auto const fr = n::rt::frun{tt, rtt, t.r_};\n              auto is_first_part = true;\n              auto const write_run_leg = [&](auto,\n                                             n::interval<n::stop_idx_t> const\n                                                 subrange) {\n                auto const common_stops = subrange.intersect(t.stop_range_);\n                if (common_stops.size() <= 1) {\n                  return;\n                }\n\n                get_is_unique_stop_name(fr, common_stops, is_unique);\n\n                auto const enter_stop = fr[common_stops.from_];\n                auto const exit_stop = fr[common_stops.to_ - 1U];\n                auto const color =\n                    enter_stop.get_route_color(n::event_type::kDep);\n                auto const& agency =\n                    enter_stop.get_provider(n::event_type::kDep);\n                auto const fare_indices = get_fare_indices(fares, j_leg);\n\n                auto const src = [&]() {\n                  if (!fr.is_scheduled()) {\n                    return n::source_idx_t::invalid();\n                  }\n                  auto const trip =\n                      enter_stop.get_trip_idx(n::event_type::kDep);\n                  auto const id_idx = tt.trip_ids_[trip].front();\n                  return tt.trip_id_src_[id_idx];\n                }();\n                auto const [service_day, _] =\n                    enter_stop.get_trip_start(n::event_type::kDep);\n\n                auto& leg = itinerary.legs_.emplace_back(api::Leg{\n                    .mode_ = to_mode(enter_stop.get_clasz(n::event_type::kDep),\n                                     api_version),\n                    .from_ = to_place(enter_stop, n::event_type::kDep),\n                    .to_ = to_place(exit_stop, n::event_type::kArr),\n                    .duration_ =\n                        std::chrono::duration_cast<std::chrono::seconds>(\n                            exit_stop.time(n::event_type::kArr) -\n                            enter_stop.time(n::event_type::kDep))\n                            .count(),\n                    .startTime_ = enter_stop.time(n::event_type::kDep),\n                    .endTime_ = exit_stop.time(n::event_type::kArr),\n                    .scheduledStartTime_ =\n                        enter_stop.scheduled_time(n::event_type::kDep),\n                    .scheduledEndTime_ =\n                        exit_stop.scheduled_time(n::event_type::kArr),\n                    .realTime_ = fr.is_rt(),\n                    .scheduled_ = fr.is_scheduled(),\n                    .interlineWithPreviousLeg_ = !is_first_part,\n                    .headsign_ = std::string{enter_stop.direction(\n                        lang, n::event_type::kDep)},\n                    .tripFrom_ =\n                        [&]() {\n                          auto const first = exit_stop.get_first_trip_stop(\n                              n::event_type::kArr);\n                          auto p = to_place(first, n::event_type::kDep);\n                          p.departure_ = first.time(n::event_type::kDep);\n                          p.scheduledDeparture_ =\n                              first.scheduled_time(n::event_type::kDep);\n                          return p;\n                        }(),\n                    .tripTo_ =\n                        [&]() {\n                          auto const last = enter_stop.get_last_trip_stop(\n                              n::event_type::kDep);\n                          auto p = to_place(last, n::event_type::kArr);\n                          p.arrival_ = last.time(n::event_type::kArr);\n                          p.scheduledArrival_ =\n                              last.scheduled_time(n::event_type::kArr);\n                          return p;\n                        }(),\n                    .category_ =\n                        enter_stop.get_category(n::event_type::kDep)\n                            .transform([&](nigiri::category_idx_t const c) {\n                              auto const& cat = tt.categories_.at(c);\n                              return api::Category{\n                                  .id_ = std::string{tt.strings_.get(cat.id_)},\n                                  .name_ = std::string{tt.translate(lang,\n                                                                    cat.name_)},\n                                  .shortName_ = std::string{tt.translate(\n                                      lang, cat.short_name_)},\n                              };\n                            }),\n                    .routeId_ = tags.route_id(enter_stop, n::event_type::kDep),\n                    .routeUrl_ = std::string{enter_stop.route_url(\n                        n::event_type::kDep, lang)},\n                    .directionId_ =\n                        enter_stop.get_direction_id(n::event_type::kDep) == 0\n                            ? \"0\"\n                            : \"1\",\n                    .routeColor_ = to_str(color.color_),\n                    .routeTextColor_ = to_str(color.text_color_),\n                    .routeType_ =\n                        enter_stop.route_type(n::event_type::kDep)\n                            .transform([](auto&& x) { return to_idx(x); }),\n                    .agencyName_ =\n                        std::string{tt.translate(lang, agency.name_)},\n                    .agencyUrl_ = std::string{tt.translate(lang, agency.url_)},\n                    .agencyId_ =\n                        std::string{\n                            tt.strings_.try_get(agency.id_).value_or(\"?\")},\n                    .tripId_ = tags.id(tt, enter_stop, n::event_type::kDep),\n                    .routeShortName_ = {std::string{\n                        api_version > 3 ? enter_stop.route_short_name(\n                                              n::event_type::kDep, lang)\n                                        : enter_stop.display_name(\n                                              n::event_type::kDep, lang)}},\n                    .routeLongName_ = {std::string{\n                        enter_stop.route_long_name(n::event_type::kDep, lang)}},\n                    .tripShortName_ = {std::string{\n                        enter_stop.trip_short_name(n::event_type::kDep, lang)}},\n                    .displayName_ = {std::string{\n                        enter_stop.display_name(n::event_type::kDep, lang)}},\n                    .cancelled_ = fr.is_cancelled(),\n                    .source_ = fmt::to_string(fr.dbg()),\n                    .fareTransferIndex_ = fare_indices.transform(\n                        [](auto&& x) { return x.transfer_idx_; }),\n                    .effectiveFareLegIndex_ = fare_indices.transform(\n                        [](auto&& x) { return x.effective_fare_leg_idx_; }),\n                    .alerts_ = get_alerts(fr, std::nullopt, false, lang),\n                    .loopedCalendarSince_ =\n                        (fr.is_scheduled() &&\n                         src != n::source_idx_t::invalid() &&\n                         tt.src_end_date_[src] < service_day)\n                            ? std::optional{tt.src_end_date_[src]}\n                            : std::nullopt,\n                    .bikesAllowed_ =\n                        enter_stop.bikes_allowed(nigiri::event_type::kDep)});\n\n                auto const attributes =\n                    tt.attribute_combinations_[enter_stop\n                                                   .get_attribute_combination(\n                                                       n::event_type::kDep)];\n                if (!leg.alerts_ && !attributes.empty()) {\n                  leg.alerts_ = std::vector<api::Alert>{};\n                }\n                for (auto const& a : attributes) {\n                  leg.alerts_->push_back(api::Alert{\n                      .code_ = std::string{tt.attributes_[a].code_.view()},\n                      .headerText_ = std::string{\n                          tt.translate(lang, tt.attributes_[a].text_)}});\n                }\n\n                leg.from_.vertexType_ = api::VertexTypeEnum::TRANSIT;\n                leg.from_.departure_ = leg.startTime_;\n                leg.from_.scheduledDeparture_ = leg.scheduledStartTime_;\n                leg.to_.vertexType_ = api::VertexTypeEnum::TRANSIT;\n                leg.to_.arrival_ = leg.endTime_;\n                leg.to_.scheduledArrival_ = leg.scheduledEndTime_;\n                if (detailed_legs) {\n                  auto polyline = geo::polyline{};\n                  fr.for_each_shape_point(shapes, common_stops,\n                                          [&](geo::latlng const& pos) {\n                                            polyline.emplace_back(pos);\n                                          });\n                  leg.legGeometry_ = api_version == 1\n                                         ? to_polyline<7>(polyline)\n                                         : to_polyline<6>(polyline);\n                } else {\n                  leg.legGeometry_ = empty_polyline();\n                }\n\n                auto const first =\n                    static_cast<n::stop_idx_t>(common_stops.from_ + 1U);\n                auto const last =\n                    static_cast<n::stop_idx_t>(common_stops.to_ - 1U);\n                leg.intermediateStops_ = std::vector<api::Place>{};\n                for (auto i = first; i < last; ++i) {\n                  auto const stop = fr[i];\n                  if (!with_scheduled_skipped_stops &&\n                      !stop.get_scheduled_stop().in_allowed() &&\n                      !stop.get_scheduled_stop().out_allowed() &&\n                      !stop.in_allowed() && !stop.out_allowed()) {\n                    continue;\n                  }\n                  auto& p = leg.intermediateStops_->emplace_back(\n                      to_place(stop, n::event_type::kDep));\n                  p.departure_ = stop.time(n::event_type::kDep);\n                  p.scheduledDeparture_ =\n                      stop.scheduled_time(n::event_type::kDep);\n                  p.arrival_ = stop.time(n::event_type::kArr);\n                  p.scheduledArrival_ =\n                      stop.scheduled_time(n::event_type::kArr);\n                }\n                is_first_part = false;\n              };\n\n              if (join_interlined_legs) {\n                write_run_leg(n::trip_idx_t{}, t.stop_range_);\n              } else {\n                fr.for_each_trip(write_run_leg);\n              }\n            },\n            [&](n::footpath) {\n              append(w && l && detailed_transfers\n                         ? street_routing(\n                               *w, *l, e, elevations, lang, from, to,\n                               default_output{\n                                   *w, car_transfers\n                                           ? osr::search_profile::kCar\n                                           : to_profile(api::ModeEnum::WALK,\n                                                        pedestrian_profile,\n                                                        elevation_costs)},\n                               j_leg.dep_time_, j_leg.arr_time_,\n                               car_transfers ? 250.0\n                                             : timetable_max_matching_distance,\n                               osr_params, cache, *blocked_mem, api_version,\n                               true,\n                               std::chrono::duration_cast<std::chrono::seconds>(\n                                   j_leg.arr_time_ - j_leg.dep_time_) +\n                                   std::chrono::minutes{10})\n                         : dummy_itinerary(from, to, api::ModeEnum::WALK,\n                                           j_leg.dep_time_, j_leg.arr_time_));\n            },\n            [&](n::routing::offset const x) {\n              if ((j_leg_idx == 0 || j_leg_idx == j.legs_.size() - 1) &&\n                  j_leg.dep_time_ == j_leg.arr_time_) {\n                return;\n              }\n\n              auto out = std::unique_ptr<output>{};\n              if (flex::mode_id::is_flex(x.transport_mode_id_)) {\n                out = std::make_unique<flex::flex_output>(\n                    *w, *l, pl, matches, ae, tz_map, tags, tt, *fl,\n                    flex::mode_id{x.transport_mode_id_});\n              } else if (x.transport_mode_id_ >= kGbfsTransportModeIdOffset) {\n                auto const is_pre_transit = pred == nullptr;\n                out = std::make_unique<gbfs::gbfs_output>(\n                    *w, gbfs_rd, gbfs_rd.get_products_ref(x.transport_mode_id_),\n                    is_pre_transit ? ignore_start_rental_return_constraints\n                                   : ignore_dest_rental_return_constraints);\n              } else {\n                out =\n                    std::make_unique<default_output>(*w, x.transport_mode_id_);\n              }\n\n              append(street_routing(\n                  *w, *l, e, elevations, lang, from, to, *out, j_leg.dep_time_,\n                  j_leg.arr_time_, max_matching_distance, osr_params, cache,\n                  *blocked_mem, api_version, detailed_legs,\n                  std::chrono::duration_cast<std::chrono::seconds>(\n                      j_leg.arr_time_ - j_leg.dep_time_) +\n                      std::chrono::minutes{5}));\n            }},\n        j_leg.uses_);\n  }\n\n  cleanup_intermodal(itinerary);\n\n  return itinerary;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/logging.cc",
    "content": "#include \"motis/logging.h\"\n\n#include <algorithm>\n#include <iostream>\n#include <string_view>\n\n#include \"fmt/ostream.h\"\n\n#include \"utl/logging.h\"\n\n#include \"nigiri/logging.h\"\n\nnamespace motis {\n\nint set_log_level(std::string_view log_lvl) {\n  if (log_lvl == \"error\") {\n    utl::log_verbosity = utl::log_level::error;\n    nigiri::s_verbosity = nigiri::log_lvl::error;\n  } else if (log_lvl == \"info\") {\n    utl::log_verbosity = utl::log_level::info;\n    nigiri::s_verbosity = nigiri::log_lvl::info;\n  } else if (log_lvl == \"debug\") {\n    utl::log_verbosity = utl::log_level::debug;\n    nigiri::s_verbosity = nigiri::log_lvl::debug;\n  } else {\n    fmt::println(std::cerr, \"Unsupported log level '{}'\\n\", log_lvl);\n    return 1;\n  }\n  return 0;\n};\n\nint set_log_level(config const& c) {\n  if (c.logging_ && c.logging_->log_level_) {\n    return set_log_level(*c.logging_->log_level_);\n  }\n  return 0;\n}\n\nint set_log_level(std::string&& log_lvl) {\n  // Support uppercase for command line option\n  std::transform(log_lvl.begin(), log_lvl.end(), log_lvl.begin(),\n                 [](unsigned char const c) { return std::tolower(c); });\n  return set_log_level(log_lvl);\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/match_platforms.cc",
    "content": "#include \"motis/match_platforms.h\"\n\n#include <filesystem>\n\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/parallel_for.h\"\n#include \"utl/parser/arg_parser.h\"\n\n#include \"osr/geojson.h\"\n#include \"osr/location.h\"\n\n#include \"motis/location_routes.h\"\n#include \"motis/osr/parameters.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nbool is_number(char const x) { return x >= '0' && x <= '9'; }\n\ntemplate <typename Fn>\nvoid for_each_number(std::string_view x, Fn&& fn) {\n  for (auto i = 0U; i < x.size(); ++i) {\n    if (!is_number(x[i])) {\n      continue;\n    }\n\n    auto j = i + 1U;\n    for (; j != x.size(); ++j) {\n      if (!is_number(x[j])) {\n        break;\n      }\n    }\n\n    fn(utl::parse<unsigned>(x.substr(i, j - i)));\n    i = j;\n  }\n}\n\nbool has_number_match(std::string_view a, std::string_view b) {\n  auto match = false;\n  for_each_number(a, [&](unsigned const x) {\n    for_each_number(b, [&](unsigned const y) { match = (x == y); });\n  });\n  return match;\n}\n\ntemplate <typename Collection>\nbool has_number_match(Collection&& a, std::string_view b) {\n  return std::any_of(a.begin(), a.end(),\n                     [&](auto&& x) { return has_number_match(x.view(), b); });\n}\n\ntemplate <typename Collection>\nbool has_exact_match(Collection&& a, std::string_view b) {\n  return std::any_of(a.begin(), a.end(),\n                     [&](auto&& x) { return x.view() == b; });\n}\n\ntemplate <typename Collection>\nbool has_contains_match(Collection&& a, std::string_view b) {\n  return std::any_of(a.begin(), a.end(),\n                     [&](auto&& x) { return x.view().contains(b); });\n}\n\nstd::optional<std::string_view> get_track(std::string_view s) {\n  if (s.size() == 0 || std::isdigit(s.back()) == 0) {\n    return std::nullopt;\n  }\n  for (auto i = 0U; i != s.size(); ++i) {\n    auto const j = s.size() - i - 1U;\n    if (std::isdigit(s[j]) == 0U) {\n      return s.substr(j + 1U);\n    }\n  }\n  return s;\n}\n\ntemplate <typename Collection>\ndouble get_routes_bonus(n::timetable const& tt,\n                        n::location_idx_t const l,\n                        Collection&& names) {\n  auto matches = 0U;\n  for (auto const& r : get_location_routes(tt, l)) {\n    for (auto const& x : names) {\n      if (r == x.view()) {\n        ++matches;\n      }\n\n      utl::for_each_token(x.view(), ' ', [&](auto&& token) {\n        if (r == token.view()) {\n          ++matches;\n        }\n      });\n    }\n  }\n\n  return matches * 20U;\n}\n\ntemplate <typename Collection>\ndouble get_match_bonus(Collection&& names,\n                       std::string_view ref,\n                       std::string_view name) {\n  auto bonus = 0U;\n  auto const size = static_cast<double>(name.size());\n  if (has_exact_match(names, ref)) {\n    bonus += std::max(0.0, 200.0 - size);\n  }\n  if (has_number_match(names, name)) {\n    bonus += std::max(0.0, 140.0 - size);\n  }\n  if (auto const track = get_track(ref);\n      track.has_value() && has_number_match(names, *track)) {\n    bonus += std::max(0.0, 20.0 - size);\n  }\n  if (has_exact_match(names, name)) {\n    bonus += std::max(0.0, 15.0 - size);\n  }\n  if (has_contains_match(names, ref)) {\n    bonus += std::max(0.0, 5.0 - size);\n  }\n  return bonus;\n}\n\ntemplate <typename Collection>\nint compare_platform_code(Collection&& names, std::string_view platform_code) {\n  auto first_match = true;\n  auto bonus = 0;\n  for (auto const& x : names) {\n    if (std::string_view{x} == platform_code) {\n      bonus += first_match ? 50 : 15;\n      first_match = false;\n    }\n  }\n  return bonus;\n}\n\nstruct center {\n  template <typename T>\n  void add(T const& polyline) {\n    for (auto const& x : polyline) {\n      add(geo::latlng(x));\n    }\n  }\n\n  void add(geo::latlng const& pos) {\n    sum_.lat_ += pos.lat();\n    sum_.lng_ += pos.lng();\n    n_ += 1U;\n  }\n\n  geo::latlng get_center() const { return {sum_.lat_ / n_, sum_.lng_ / n_}; }\n\n  geo::latlng sum_;\n  std::size_t n_;\n};\n\nstd::optional<geo::latlng> get_platform_center(osr::platforms const& pl,\n                                               osr::ways const& w,\n                                               osr::platform_idx_t const x) {\n  auto c = center{};\n  for (auto const p : pl.platform_ref_[x]) {\n    std::visit(utl::overloaded{[&](osr::node_idx_t const node) {\n                                 c.add(pl.get_node_pos(node).as_latlng());\n                               },\n                               [&](osr::way_idx_t const way) {\n                                 c.add(w.way_polylines_[way]);\n                               }},\n               osr::to_ref(p));\n  }\n  if (c.n_ == 0U) {\n    return std::nullopt;\n  }\n\n  auto const center = c.get_center();\n  auto const lng_dist = geo::approx_distance_lng_degrees(center);\n\n  auto closest = geo::latlng{};\n  auto update_closest = [&, squared_dist = std::numeric_limits<double>::max()](\n                            geo::latlng const& candidate,\n                            double const candidate_squared_dist) mutable {\n    if (candidate_squared_dist < squared_dist) {\n      closest = candidate;\n      squared_dist = candidate_squared_dist;\n    }\n  };\n  for (auto const p : pl.platform_ref_[x]) {\n    std::visit(\n        utl::overloaded{\n            [&](osr::node_idx_t const node) {\n              auto const candidate = pl.get_node_pos(node).as_latlng();\n              update_closest(candidate, geo::approx_squared_distance(\n                                            candidate, center, lng_dist));\n            },\n            [&](osr::way_idx_t const way) {\n              for (auto const [a, b] : utl::pairwise(w.way_polylines_[way])) {\n                auto const [best, squared_dist] =\n                    geo::approx_closest_on_segment(center, a, b, lng_dist);\n                update_closest(best, squared_dist);\n              }\n            }},\n        osr::to_ref(p));\n  }\n  return closest;\n}\n\nvector_map<n::location_idx_t, osr::platform_idx_t> get_matches(\n    nigiri::timetable const& tt, osr::platforms const& pl, osr::ways const& w) {\n  auto m = n::vector_map<n::location_idx_t, osr::platform_idx_t>{};\n  m.resize(tt.n_locations());\n  utl::parallel_for_run(tt.n_locations(), [&](auto const i) {\n    auto const l = n::location_idx_t{i};\n    m[l] = get_match(tt, pl, w, l);\n  });\n  return m;\n}\n\nosr::platform_idx_t get_match(n::timetable const& tt,\n                              osr::platforms const& pl,\n                              osr::ways const& w,\n                              n::location_idx_t const l) {\n  auto const ref = tt.locations_.coordinates_[l];\n  auto best = osr::platform_idx_t::invalid();\n  auto best_score = std::numeric_limits<double>::max();\n\n  pl.find(ref, [&](osr::platform_idx_t const x) {\n    auto const center = get_platform_center(pl, w, x);\n    if (!center.has_value()) {\n      return;\n    }\n\n    auto const dist = geo::distance(*center, ref);\n    auto const match_bonus =\n        get_match_bonus(pl.platform_names_[x], tt.locations_.ids_[l].view(),\n                        tt.get_default_translation(tt.locations_.names_[l]));\n    auto const lvl = pl.get_level(w, x);\n    auto const lvl_bonus =\n        lvl != osr::kNoLevel && lvl.to_float() != 0.0F ? 5 : 0;\n    auto const way_bonus = osr::is_way(pl.platform_ref_[x].front()) ? 20 : 0;\n    auto const routes_bonus = get_routes_bonus(tt, l, pl.platform_names_[x]);\n    auto const code_bonus = compare_platform_code(\n        pl.platform_names_[x],\n        tt.get_default_translation(tt.locations_.platform_codes_[l]));\n\n    auto const score =\n        dist - match_bonus - way_bonus - lvl_bonus - routes_bonus - code_bonus;\n    if (score < best_score) {\n      best = x;\n      best_score = score;\n    }\n  });\n\n  if (best != osr::platform_idx_t::invalid()) {\n    get_match_bonus(pl.platform_names_[best], tt.locations_.ids_[l].view(),\n                    tt.get_default_translation(tt.locations_.names_[l]));\n  }\n\n  return best;\n}\n\nway_matches_storage::way_matches_storage(std::filesystem::path path,\n                                         cista::mmap::protection const mode,\n                                         double const max_matching_distance)\n    : mode_{mode},\n      p_{[&]() {\n        std::filesystem::create_directories(path);\n        return std::move(path);\n      }()},\n      matches_{osr::mm_vec<osr::raw_way_candidate>{mm(\"way_matches.bin\")},\n               osr::mm_vec<cista::base_t<n::location_idx_t>>{\n                   mm(\"way_matches_idx.bin\")}},\n      max_matching_distance_{max_matching_distance} {}\n\ncista::mmap way_matches_storage::mm(char const* file) {\n  return cista::mmap{(p_ / file).generic_string().c_str(), mode_};\n}\n\nvoid way_matches_storage::preprocess_osr_matches(\n    nigiri::timetable const& tt,\n    osr::platforms const& pl,\n    osr::ways const& w,\n    osr::lookup const& lookup,\n    platform_matches_t const& platform_matches) {\n  auto const pt = utl::get_active_progress_tracker();\n  pt->in_high(tt.n_locations());\n\n  utl::parallel_ordered_collect_threadlocal<int>(\n      tt.n_locations(),\n      [&](int, std::size_t const idx) {\n        auto const l =\n            n::location_idx_t{static_cast<n::location_idx_t::value_t>(idx)};\n        return lookup.get_raw_match(\n            osr::location{tt.locations_.coordinates_[l],\n                          pl.get_level(w, platform_matches[l])},\n            max_matching_distance_);\n      },\n      [&](std::size_t, std::vector<osr::raw_way_candidate>&& l) {\n        matches_.emplace_back(l);\n      },\n      pt->update_fn());\n}\n\nstd::vector<osr::match_t> get_reverse_platform_way_matches(\n    osr::lookup const& lookup,\n    way_matches_storage const* way_matches,\n    osr::search_profile const p,\n    std::span<nigiri::location_idx_t const> const locations,\n    std::span<osr::location const> const osr_locations,\n    osr::direction const dir,\n    double const max_matching_distance) {\n  auto const use_raw_matches =\n      way_matches && !way_matches->matches_.empty() &&\n      way_matches->max_matching_distance_ >= max_matching_distance;\n  return utl::to_vec(\n      utl::zip(locations, osr_locations),\n      [&](std::tuple<n::location_idx_t, osr::location> const ll) {\n        auto const& [l, query] = ll;\n        auto raw_matches =\n            std::optional<std::span<osr::raw_way_candidate const>>{};\n        if (use_raw_matches) {\n          auto const& m = way_matches->matches_[l];\n          raw_matches = {m.begin(), m.end()};\n        }\n        return lookup.match(to_profile_parameters(p, {}), query, true, dir,\n                            max_matching_distance, nullptr, p, raw_matches);\n      });\n};\n\n}  // namespace motis\n"
  },
  {
    "path": "src/metrics_registry.cc",
    "content": "#include \"motis/metrics_registry.h\"\n#include \"prometheus/histogram.h\"\n\nnamespace motis {\n\nmetrics_registry::metrics_registry()\n    : metrics_registry(\n          prometheus::Histogram::BucketBoundaries{\n              0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 75, 100, 1000},\n          prometheus::Histogram::BucketBoundaries{\n              0.01, 0.1, 0.5, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 45, 60}) {}\n\nmetrics_registry::metrics_registry(\n    prometheus::Histogram::BucketBoundaries event_boundaries,\n    prometheus::Histogram::BucketBoundaries time_boundaries)\n    : registry_{prometheus::Registry()},\n      routing_requests_{prometheus::BuildCounter()\n                            .Name(\"motis_routing_requests_total\")\n                            .Help(\"Number of routing requests\")\n                            .Register(registry_)\n                            .Add({})},\n      one_to_many_requests_{prometheus::BuildCounter()\n                                .Name(\"motis_one_to_many_requests_total\")\n                                .Help(\"Number of one to many requests\")\n                                .Register(registry_)\n                                .Add({})},\n      routing_journeys_found_{prometheus::BuildCounter()\n                                  .Name(\"motis_routing_journeys_found_total\")\n                                  .Help(\"Number of journey results\")\n                                  .Register(registry_)\n                                  .Add({})},\n      routing_odm_journeys_found_{\n          prometheus::BuildHistogram()\n              .Name(\"motis_routing_odm_events_found\")\n              .Help(\"Number of journey results including an ODM part\")\n              .Register(registry_)},\n      routing_odm_journeys_found_blacklist_{routing_odm_journeys_found_.Add(\n          {{\"stage\", \"blacklist\"}},\n          prometheus::Histogram::BucketBoundaries{0, 500, 1000, 2000, 3000,\n                                                  4000, 5000, 10000, 15000,\n                                                  20000, 50000})},\n      routing_odm_journeys_found_whitelist_{routing_odm_journeys_found_.Add(\n          {{\"stage\", \"whitelist\"}}, event_boundaries)},\n      routing_odm_journeys_found_non_dominated_pareto_{\n          routing_odm_journeys_found_.Add({{\"stage\", \"non_dominated_pareto\"}},\n                                          event_boundaries)},\n      routing_odm_journeys_found_non_dominated_cost_{\n          routing_odm_journeys_found_.Add({{\"stage\", \"non_dominated_cost\"}},\n                                          event_boundaries)},\n      routing_odm_journeys_found_non_dominated_prod_{\n          routing_odm_journeys_found_.Add({{\"stage\", \"non_dominated_prod\"}},\n                                          event_boundaries)},\n      routing_odm_journeys_found_non_dominated_{routing_odm_journeys_found_.Add(\n          {{\"stage\", \"non_dominated\"}}, event_boundaries)},\n      routing_journey_duration_seconds_{\n          prometheus::BuildHistogram()\n              .Name(\"motis_routing_journey_duration_seconds\")\n              .Help(\"Journey duration statistics\")\n              .Register(registry_)\n              .Add({},\n                   prometheus::Histogram::BucketBoundaries{\n                       300, 600, 1200, 1800, 3600, 7200, 10800, 14400, 18000,\n                       21600, 43200, 86400})},\n      routing_execution_duration_seconds_{\n          prometheus::BuildHistogram()\n              .Name(\"motis_routing_execution_duration_seconds\")\n              .Help(\"Routing execution duration statistics\")\n              .Register(registry_)},\n      routing_execution_duration_seconds_init_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"init\"}},\n                                                  time_boundaries)},\n      routing_execution_duration_seconds_blacklisting_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"blacklisting\"}},\n                                                  time_boundaries)},\n      routing_execution_duration_seconds_preparing_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"preparing\"}},\n                                                  time_boundaries)},\n      routing_execution_duration_seconds_routing_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"routing\"}},\n                                                  time_boundaries)},\n      routing_execution_duration_seconds_whitelisting_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"whitelisting\"}},\n                                                  time_boundaries)},\n      routing_execution_duration_seconds_mixing_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"mixing\"}},\n                                                  time_boundaries)},\n      routing_execution_duration_seconds_total_{\n          routing_execution_duration_seconds_.Add({{\"stage\", \"total\"}},\n                                                  time_boundaries)},\n      current_trips_running_scheduled_count_{\n          prometheus::BuildGauge()\n              .Name(\"current_trips_running_scheduled_count\")\n              .Help(\"The number of currently running transports\")\n              .Register(registry_)},\n      current_trips_running_scheduled_with_realtime_count_{\n          prometheus::BuildGauge()\n              .Name(\"current_trips_running_scheduled_with_realtime_count\")\n              .Help(\"The number of currently running transports that have RT \"\n                    \"data\")\n              .Register(registry_)},\n      total_trips_with_realtime_count_{\n          prometheus::BuildGauge()\n              .Name(\"total_trips_with_realtime_count\")\n              .Help(\"The total number of transports that have RT data\")\n              .Register(registry_)\n              .Add({})},\n      timetable_first_day_timestamp_{\n          prometheus::BuildGauge()\n              .Name(\"nigiri_timetable_first_day_timestamp_seconds\")\n              .Help(\"Day of the first transport in unixtime\")\n              .Register(registry_)},\n      timetable_last_day_timestamp_{\n          prometheus::BuildGauge()\n              .Name(\"nigiri_timetable_last_day_timestamp_seconds\")\n              .Help(\"Day of the last transport in unixtime\")\n              .Register(registry_)},\n      timetable_locations_count_{\n          prometheus::BuildGauge()\n              .Name(\"nigiri_timetable_locations_count\")\n              .Help(\"The number of locations in the timetable\")\n              .Register(registry_)},\n      timetable_trips_count_{prometheus::BuildGauge()\n                                 .Name(\"nigiri_timetable_trips_count\")\n                                 .Help(\"The number of trips in the timetable\")\n                                 .Register(registry_)},\n      timetable_transports_x_days_count_{\n          prometheus::BuildGauge()\n              .Name(\"nigiri_timetable_transports_x_days_count\")\n              .Help(\"The number of transports x service days in the timetable\")\n              .Register(registry_)} {}\n\nmetrics_registry::~metrics_registry() = default;\n\n}  // namespace motis\n"
  },
  {
    "path": "src/odm/blacklist_taxi.cc",
    "content": "#include \"motis/odm/prima.h\"\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/json.hpp\"\n\n#include \"nigiri/timetable.h\"\n\n#include \"utl/erase_if.h\"\n\n#include \"motis/http_req.h\"\n\nnamespace n = nigiri;\nnamespace json = boost::json;\nusing namespace std::chrono_literals;\n\nnamespace motis::odm {\n\njson::array to_json(std::vector<n::routing::offset> const& offsets,\n                    n::timetable const& tt) {\n  auto a = json::array{};\n  for (auto const& o : offsets) {\n    auto const& pos = tt.locations_.coordinates_[o.target_];\n    a.emplace_back(json::value{{\"lat\", pos.lat_}, {\"lng\", pos.lng_}});\n  }\n  return a;\n}\n\nstd::string prima::make_blacklist_taxi_request(\n    n::timetable const& tt,\n    n::interval<n::unixtime_t> const& taxi_intvl) const {\n  return json::serialize(json::value{\n      {\"start\", {{\"lat\", from_.pos_.lat_}, {\"lng\", from_.pos_.lng_}}},\n      {\"target\", {{\"lat\", to_.pos_.lat_}, {\"lng\", to_.pos_.lng_}}},\n      {\"startBusStops\", to_json(first_mile_taxi_, tt)},\n      {\"targetBusStops\", to_json(last_mile_taxi_, tt)},\n      {\"earliest\", to_millis(taxi_intvl.from_)},\n      {\"latest\", to_millis(taxi_intvl.to_)},\n      {\"startFixed\", fixed_ == n::event_type::kDep},\n      {\"capacities\", json::value_from(cap_)}});\n}\n\nn::interval<n::unixtime_t> read_intvl(json::value const& jv) {\n  return n::interval{to_unix(jv.as_object().at(\"startTime\").as_int64()),\n                     to_unix(jv.as_object().at(\"endTime\").as_int64())};\n}\n\nbool prima::consume_blacklist_taxi_response(std::string_view json) {\n  auto const read_service_times = [&](json::array const& blacklist_times,\n                                      auto const& offsets, auto& taxi_times) {\n    if (blacklist_times.size() != offsets.size()) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[blacklist taxi] #intervals mismatch\");\n      taxi_times.clear();\n      return;\n    }\n\n    taxi_times.resize(offsets.size());\n\n    for (auto [blacklist_time, taxi_time] :\n         utl::zip(blacklist_times, taxi_times)) {\n      for (auto const& t : blacklist_time.as_array()) {\n        taxi_time.emplace_back(read_intvl(t));\n      }\n    }\n  };\n\n  auto const update_direct_rides = [&](json::array const& direct_times) {\n    utl::erase_if(direct_taxi_, [&](auto const& ride) {\n      return utl::none_of(direct_times, [&](auto const& t) {\n        auto const i = read_intvl(t);\n        return i.contains(ride.dep_) && i.contains(ride.arr_);\n      });\n    });\n  };\n\n  try {\n    auto const o = json::parse(json).as_object();\n\n    read_service_times(o.at(\"start\").as_array(), first_mile_taxi_,\n                       first_mile_taxi_times_);\n    read_service_times(o.at(\"target\").as_array(), last_mile_taxi_,\n                       last_mile_taxi_times_);\n\n    if (direct_duration_ && *direct_duration_ < kODMMaxDuration) {\n      update_direct_rides(o.at(\"direct\").as_array());\n    }\n\n  } catch (std::exception const&) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[blacklist taxi] could not parse response: {}\", json);\n    return false;\n  }\n\n  return true;\n}\n\nbool prima::blacklist_taxi(n::timetable const& tt,\n                           n::interval<n::unixtime_t> const& taxi_intvl) {\n  auto blacklist_response = std::optional<std::string>{};\n  auto ioc = boost::asio::io_context{};\n  try {\n    n::log(n::log_lvl::debug, \"motis.prima\", \"[blacklist taxi] request for {}\",\n           taxi_intvl);\n    boost::asio::co_spawn(\n        ioc,\n        [&]() -> boost::asio::awaitable<void> {\n          auto const prima_msg = co_await http_POST(\n              taxi_blacklist_, kReqHeaders,\n              make_blacklist_taxi_request(tt, taxi_intvl), 10s);\n          blacklist_response = get_http_body(prima_msg);\n        },\n        boost::asio::detached);\n    ioc.run();\n  } catch (std::exception const& e) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[blacklist taxi] networking failed: {}\", e.what());\n    blacklist_response = std::nullopt;\n  }\n  if (!blacklist_response) {\n    return false;\n  }\n\n  return consume_blacklist_taxi_response(*blacklist_response);\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/bounds.cc",
    "content": "#include \"motis/odm/bounds.h\"\n\n#include \"tg.h\"\n\n#include \"fmt/std.h\"\n\n#include \"utl/verify.h\"\n\n#include \"cista/mmap.h\"\n\nnamespace fs = std::filesystem;\n\nnamespace motis::odm {\n\nbounds::bounds(fs::path const& p) {\n  auto const f =\n      cista::mmap{p.generic_string().c_str(), cista::mmap::protection::READ};\n\n  geom_ = tg_parse_geojsonn(f.view().data(), f.size());\n\n  if (tg_geom_error(geom_)) {\n    char const* err = tg_geom_error(geom_);\n    fmt::println(\"Error parsing ODM Bounds GeoJSON: {}\", err);\n    tg_geom_free(geom_);\n    throw utl::fail(\"unable to parse {}: {}\", p, err);\n  }\n\n  return;\n}\n\nbounds::~bounds() { tg_geom_free(geom_); }\n\nbool bounds::contains(geo::latlng const& x) const {\n  auto const point = tg_geom_new_point(tg_point{x.lng(), x.lat()});\n  auto const result = tg_geom_within(point, geom_);\n  tg_geom_free(point);\n  return result;\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/journeys.cc",
    "content": "#include \"motis/odm/journeys.h\"\n\n#include <charconv>\n\n#include \"utl/parser/buf_reader.h\"\n#include \"utl/parser/csv_range.h\"\n#include \"utl/parser/line_range.h\"\n\n#include \"nigiri/common/parse_time.h\"\n#include \"nigiri/routing/pareto_set.h\"\n\n#include \"motis/odm/odm.h\"\n#include \"motis/transport_mode_ids.h\"\n\nnamespace motis::odm {\n\nstruct csv_journey {\n  utl::csv_col<utl::cstr, UTL_NAME(\"departure\")> departure_time_;\n  utl::csv_col<utl::cstr, UTL_NAME(\"arrival\")> arrival_time_;\n  utl::csv_col<std::uint8_t, UTL_NAME(\"transfers\")> transfers_;\n  utl::csv_col<utl::cstr, UTL_NAME(\"first_mile_mode\")> first_mile_mode_;\n  utl::csv_col<nigiri::duration_t::rep, UTL_NAME(\"first_mile_duration\")>\n      first_mile_duration_;\n  utl::csv_col<utl::cstr, UTL_NAME(\"last_mile_mode\")> last_mile_mode_;\n  utl::csv_col<nigiri::duration_t::rep, UTL_NAME(\"last_mile_duration\")>\n      last_mile_duration_;\n};\n\nstd::optional<nigiri::transport_mode_id_t> read_transport_mode(\n    std::string_view m) {\n  if (m == \"taxi\") {\n    return kOdmTransportModeId;\n  } else if (m == \"walk\") {\n    return kWalkTransportModeId;\n  } else {\n    return std::nullopt;\n  }\n}\n\nnigiri::routing::journey make_dummy(\n    nigiri::unixtime_t const departure,\n    nigiri::unixtime_t const arrival,\n    std::uint8_t const transfers,\n    nigiri::transport_mode_id_t const first_mile_mode,\n    nigiri::duration_t const first_mile_duration,\n    nigiri::transport_mode_id_t const last_mile_mode,\n    nigiri::duration_t const last_mile_duration) {\n  return nigiri::routing::journey{\n      .legs_ = {{nigiri::direction::kForward, nigiri::location_idx_t::invalid(),\n                 nigiri::location_idx_t::invalid(), departure,\n                 departure + first_mile_duration,\n                 nigiri::routing::offset{nigiri::location_idx_t::invalid(),\n                                         first_mile_duration, first_mile_mode}},\n                {nigiri::direction::kForward, nigiri::location_idx_t::invalid(),\n                 nigiri::location_idx_t::invalid(),\n                 arrival - last_mile_duration, arrival,\n                 nigiri::routing::offset{nigiri::location_idx_t::invalid(),\n                                         last_mile_duration, last_mile_mode}}},\n      .start_time_ = departure,\n      .dest_time_ = arrival,\n      .transfers_ = transfers};\n}\n\nstd::vector<nigiri::routing::journey> from_csv(std::string_view const csv) {\n  auto journeys = std::vector<nigiri::routing::journey>{};\n  utl::line_range{utl::make_buf_reader(csv)} | utl::csv<csv_journey>() |\n      utl::for_each([&](csv_journey const& cj) {\n        try {\n          auto const departure =\n              nigiri::parse_time(cj.departure_time_->trim().view(), \"%F %R\");\n\n          auto const arrival =\n              nigiri::parse_time(cj.arrival_time_->trim().view(), \"%F %R\");\n\n          auto const first_mile_duration =\n              nigiri::duration_t{*cj.first_mile_duration_};\n          auto const first_mile_mode =\n              read_transport_mode(cj.first_mile_mode_->trim().view());\n          if (!first_mile_mode) {\n            fmt::println(\"Invalid first-mile transport mode: {}\",\n                         cj.first_mile_mode_->view());\n            return;\n          }\n\n          auto const last_mile_duration =\n              nigiri::duration_t{*cj.last_mile_duration_};\n          auto const last_mile_mode =\n              read_transport_mode(cj.last_mile_mode_->trim().view());\n          if (!last_mile_mode) {\n            fmt::println(\"Invalid last-mile transport mode: {}\",\n                         cj.last_mile_mode_->view());\n            return;\n          }\n\n          journeys.push_back(make_dummy(departure, arrival, *cj.transfers_,\n                                        *first_mile_mode, first_mile_duration,\n                                        *last_mile_mode, last_mile_duration));\n\n        } catch (std::exception const& e) {\n          fmt::println(\"could not parse csv_journey: {}\", e.what());\n        }\n      });\n\n  return journeys;\n}\n\nnigiri::pareto_set<nigiri::routing::journey> separate_pt(\n    std::vector<nigiri::routing::journey>& journeys) {\n  auto pt_journeys = nigiri::pareto_set<nigiri::routing::journey>{};\n  for (auto j = begin(journeys); j != end(journeys);) {\n    if (is_pure_pt(*j)) {\n      pt_journeys.add(std::move(*j));\n      j = journeys.erase(j);\n    } else {\n      ++j;\n    }\n  }\n  return pt_journeys;\n}\n\nstd::string to_csv(nigiri::routing::journey const& j) {\n  auto const mode_str = [&](nigiri::transport_mode_id_t const mode) {\n    return mode == kOdmTransportModeId ? \"taxi\" : \"walk\";\n  };\n\n  auto const first_mile_mode =\n      !j.legs_.empty() && std::holds_alternative<nigiri::routing::offset>(\n                              j.legs_.front().uses_)\n          ? mode_str(std::get<nigiri::routing::offset>(j.legs_.front().uses_)\n                         .transport_mode_id_)\n          : \"walk\";\n\n  auto const first_mile_duration =\n      !j.legs_.empty() && std::holds_alternative<nigiri::routing::offset>(\n                              j.legs_.front().uses_)\n          ? std::get<nigiri::routing::offset>(j.legs_.front().uses_)\n                .duration()\n                .count()\n          : nigiri::duration_t::rep{0};\n\n  auto const last_mile_mode =\n      j.legs_.size() > 1 && std::holds_alternative<nigiri::routing::offset>(\n                                j.legs_.back().uses_)\n          ? mode_str(std::get<nigiri::routing::offset>(j.legs_.back().uses_)\n                         .transport_mode_id_)\n          : \"walk\";\n\n  auto const last_mile_duration =\n      j.legs_.size() > 1 && std::holds_alternative<nigiri::routing::offset>(\n                                j.legs_.back().uses_)\n          ? std::get<nigiri::routing::offset>(j.legs_.back().uses_)\n                .duration()\n                .count()\n          : nigiri::duration_t::rep{0};\n\n  return fmt::format(\"{}, {}, {}, {}, {:0>2}, {}, {:0>2}\", j.start_time_,\n                     j.dest_time_, j.transfers_, first_mile_mode,\n                     first_mile_duration, last_mile_mode, last_mile_duration);\n}\n\nstd::string to_csv(std::vector<nigiri::routing::journey> const& jv) {\n  auto ss = std::stringstream{};\n  ss << \"departure, arrival, transfers, first_mile_mode, \"\n        \"first_mile_duration, last_mile_mode, last_mile_duration\\n\";\n\n  for (auto const& j : jv) {\n    ss << to_csv(j) << \"\\n\";\n  }\n\n  return ss.str();\n}\n\nnigiri::routing::journey make_odm_direct(nigiri::location_idx_t const from,\n                                         nigiri::location_idx_t const to,\n                                         nigiri::unixtime_t const departure,\n                                         nigiri::unixtime_t const arrival) {\n  return nigiri::routing::journey{\n      .legs_ = {{nigiri::direction::kForward, from, to, departure, arrival,\n                 nigiri::routing::offset{to,\n                                         std::chrono::abs(arrival - departure),\n                                         kOdmTransportModeId}}},\n      .start_time_ = departure,\n      .dest_time_ = arrival,\n      .dest_ = to,\n      .transfers_ = 0U};\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/meta_router.cc",
    "content": "#if defined(_MSC_VER)\n// needs to be the first to include WinSock.h\n#include \"boost/asio.hpp\"\n#endif\n\n#include \"motis/odm/meta_router.h\"\n\n#include <vector>\n\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/thread/tss.hpp\"\n\n#include \"prometheus/histogram.h\"\n\n#include \"utl/erase_duplicates.h\"\n\n#include \"ctx/ctx.h\"\n\n#include \"nigiri/logging.h\"\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/routing/limits.h\"\n#include \"nigiri/routing/query.h\"\n#include \"nigiri/routing/raptor_search.h\"\n#include \"nigiri/routing/start_times.h\"\n#include \"nigiri/types.h\"\n\n#include \"osr/routing/route.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/constants.h\"\n#include \"motis/ctx_data.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/gbfs/routing_data.h\"\n#include \"motis/journey_to_response.h\"\n#include \"motis/metrics_registry.h\"\n#include \"motis/odm/bounds.h\"\n#include \"motis/odm/journeys.h\"\n#include \"motis/odm/odm.h\"\n#include \"motis/odm/prima.h\"\n#include \"motis/odm/shorten.h\"\n#include \"motis/odm/td_offsets.h\"\n#include \"motis/osr/parameters.h\"\n#include \"motis/osr/street_routing.h\"\n#include \"motis/place.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/modes_to_clasz_mask.h\"\n#include \"motis/timetable/time_conv.h\"\n#include \"motis/transport_mode_ids.h\"\n\nnamespace n = nigiri;\nusing namespace std::chrono_literals;\n\nusing td_offsets_t =\n    n::hash_map<n::location_idx_t, std::vector<n::routing::td_offset>>;\n\nnamespace motis::odm {\n\nconstexpr auto kODMLookAhead = nigiri::duration_t{24h};\nconstexpr auto kSearchIntervalSize = nigiri::duration_t{10h};\nconstexpr auto kContextPadding = nigiri::duration_t{2h};\n\nvoid print_time(auto const& start,\n                std::string_view name,\n                prometheus::Histogram& metric) {\n  auto const millis = std::chrono::duration_cast<std::chrono::milliseconds>(\n      std::chrono::steady_clock::now() - start);\n  n::log(n::log_lvl::debug, \"motis.prima\", \"{} {}\", name, millis);\n  metric.Observe(static_cast<double>(millis.count()) / 1000.0);\n}\n\nmeta_router::meta_router(ep::routing const& r,\n                         api::plan_params const& query,\n                         std::vector<api::ModeEnum> const& pre_transit_modes,\n                         std::vector<api::ModeEnum> const& post_transit_modes,\n                         std::vector<api::ModeEnum> const& direct_modes,\n                         std::variant<osr::location, tt_location> const& from,\n                         std::variant<osr::location, tt_location> const& to,\n                         api::Place const& from_p,\n                         api::Place const& to_p,\n                         nigiri::routing::query const& start_time,\n                         std::vector<api::Itinerary>& direct,\n                         nigiri::duration_t const fastest_direct,\n                         bool const odm_pre_transit,\n                         bool const odm_post_transit,\n                         bool const odm_direct,\n                         bool const ride_sharing_pre_transit,\n                         bool const ride_sharing_post_transit,\n                         bool const ride_sharing_direct,\n                         unsigned const api_version)\n    : r_{r},\n      query_{query},\n      pre_transit_modes_{pre_transit_modes},\n      post_transit_modes_{post_transit_modes},\n      direct_modes_{direct_modes},\n      from_{from},\n      to_{to},\n      from_place_{from_p},\n      to_place_{to_p},\n      start_time_{start_time},\n      direct_{direct},\n      fastest_direct_{fastest_direct},\n      odm_pre_transit_{odm_pre_transit},\n      odm_post_transit_{odm_post_transit},\n      odm_direct_{odm_direct},\n      ride_sharing_pre_transit_{ride_sharing_pre_transit},\n      ride_sharing_post_transit_{ride_sharing_post_transit},\n      ride_sharing_direct_{ride_sharing_direct},\n      api_version_{api_version},\n      tt_{r_.tt_},\n      rt_{r.rt_},\n      rtt_{rt_->rtt_.get()},\n      e_{rt_->e_.get()},\n      gbfs_rd_{r.w_, r.l_, r.gbfs_},\n      start_{query_.arriveBy_ ? to_ : from_},\n      dest_{query_.arriveBy_ ? from_ : to_},\n      start_modes_{query_.arriveBy_ ? post_transit_modes_ : pre_transit_modes_},\n      dest_modes_{query_.arriveBy_ ? pre_transit_modes_ : post_transit_modes_},\n      start_form_factors_{query_.arriveBy_\n                              ? query_.postTransitRentalFormFactors_\n                              : query_.preTransitRentalFormFactors_},\n      dest_form_factors_{query_.arriveBy_\n                             ? query_.preTransitRentalFormFactors_\n                             : query_.postTransitRentalFormFactors_},\n      start_propulsion_types_{query_.arriveBy_\n                                  ? query_.postTransitRentalPropulsionTypes_\n                                  : query_.preTransitRentalPropulsionTypes_},\n      dest_propulsion_types_{query_.arriveBy_\n                                 ? query_.preTransitRentalPropulsionTypes_\n                                 : query_.postTransitRentalPropulsionTypes_},\n      start_rental_providers_{query_.arriveBy_\n                                  ? query_.postTransitRentalProviders_\n                                  : query_.preTransitRentalProviders_},\n      dest_rental_providers_{query_.arriveBy_\n                                 ? query_.preTransitRentalProviders_\n                                 : query_.postTransitRentalProviders_},\n      start_rental_provider_groups_{\n          query_.arriveBy_ ? query_.postTransitRentalProviderGroups_\n                           : query_.preTransitRentalProviderGroups_},\n      dest_rental_provider_groups_{\n          query_.arriveBy_ ? query_.preTransitRentalProviderGroups_\n                           : query_.postTransitRentalProviderGroups_},\n      start_ignore_rental_return_constraints_{\n          query.arriveBy_ ? query_.ignorePreTransitRentalReturnConstraints_\n                          : query_.ignorePostTransitRentalReturnConstraints_},\n      dest_ignore_rental_return_constraints_{\n          query.arriveBy_ ? query_.ignorePostTransitRentalReturnConstraints_\n                          : query_.ignorePreTransitRentalReturnConstraints_} {}\n\nmeta_router::~meta_router() = default;\n\nn::routing::query meta_router::get_base_query(\n    n::interval<n::unixtime_t> const& intvl) const {\n  return {\n      .start_time_ = intvl,\n      .start_match_mode_ = motis::ep::get_match_mode(r_, start_),\n      .dest_match_mode_ = motis::ep::get_match_mode(r_, dest_),\n      .use_start_footpaths_ = !motis::ep::is_intermodal(r_, start_),\n      .max_transfers_ = static_cast<std::uint8_t>(\n          query_.maxTransfers_.has_value() ? *query_.maxTransfers_\n                                           : n::routing::kMaxTransfers),\n      .max_travel_time_ = query_.maxTravelTime_\n                              .and_then([](std::int64_t const dur) {\n                                return std::optional{n::duration_t{dur}};\n                              })\n                              .value_or(ep::kInfinityDuration),\n      .min_connection_count_ = 0U,\n      .extend_interval_earlier_ = false,\n      .extend_interval_later_ = false,\n      .max_interval_ = std::nullopt,\n      .prf_idx_ = static_cast<n::profile_idx_t>(\n          query_.useRoutedTransfers_\n              ? (query_.pedestrianProfile_ ==\n                         api::PedestrianProfileEnum::WHEELCHAIR\n                     ? 2U\n                     : 1U)\n              : 0U),\n      .allowed_claszes_ = to_clasz_mask(query_.transitModes_),\n      .require_bike_transport_ = query_.requireBikeTransport_,\n      .require_car_transport_ = query_.requireCarTransport_,\n      .transfer_time_settings_ =\n          n::routing::transfer_time_settings{\n              .default_ = (query_.minTransferTime_ == 0 &&\n                           query_.additionalTransferTime_ == 0 &&\n                           query_.transferTimeFactor_ == 1.0),\n              .min_transfer_time_ = n::duration_t{query_.minTransferTime_},\n              .additional_time_ = n::duration_t{query_.additionalTransferTime_},\n              .factor_ = static_cast<float>(query_.transferTimeFactor_)},\n      .via_stops_ =\n          motis::ep::get_via_stops(*tt_, *r_.tags_, query_.via_,\n                                   query_.viaMinimumStay_, query_.arriveBy_),\n      .fastest_direct_ = fastest_direct_ == ep::kInfinityDuration\n                             ? std::nullopt\n                             : std::optional{fastest_direct_}};\n}\n\nstd::vector<meta_router::routing_result> meta_router::search_interval(\n    std::vector<n::routing::query> const& sub_queries) const {\n  auto const tasks = utl::to_vec(sub_queries, [&](n::routing::query const& q) {\n    auto fn = [&, q = std::move(q)]() mutable {\n      auto const timeout = std::chrono::seconds{query_.timeout_.value_or(\n          r_.config_.get_limits().routing_max_timeout_seconds_)};\n      auto search_state = n::routing::search_state{};\n      auto raptor_state = n::routing::raptor_state{};\n      return routing_result{raptor_search(\n          *tt_, rtt_, search_state, raptor_state, std::move(q),\n          query_.arriveBy_ ? n::direction::kBackward : n::direction::kForward,\n          timeout)};\n    };\n    return ctx_call(ctx_data{}, std::move(fn));\n  });\n  return utl::to_vec(\n      tasks,\n      [](ctx::future_ptr<ctx_data, meta_router::routing_result> const& t) {\n        return t->val();\n      });\n}\n\nstd::vector<n::routing::journey> collect_odm_journeys(\n    std::vector<meta_router::routing_result> const& results,\n    nigiri::transport_mode_id_t const mode) {\n  auto taxi_journeys = std::vector<n::routing::journey>{};\n  for (auto const& r : results | std::views::drop(1)) {\n    for (auto const& j : r.journeys_) {\n      if (uses_odm(j, mode)) {\n        taxi_journeys.push_back(j);\n        taxi_journeys.back().transfers_ +=\n            (j.legs_.empty() || !is_odm_leg(j.legs_.front(), mode) ? 0U : 1U) +\n            (j.legs_.size() < 2U || !is_odm_leg(j.legs_.back(), mode) ? 0U\n                                                                      : 1U);\n      }\n    }\n  }\n  n::log(n::log_lvl::debug, \"motis.prima\",\n         \"[routing] collected {} mixed ODM-PT journeys for mode {}\",\n         taxi_journeys.size(), mode);\n  return taxi_journeys;\n}\n\nvoid pareto_dominance(std::vector<n::routing::journey>& odm_journeys) {\n\n  auto const pareto_dom = [](n::routing::journey const& a,\n                             n::routing::journey const& b) -> bool {\n    auto const odm_time_a = odm_time(a);\n    auto const odm_time_b = odm_time(b);\n    return a.dominates(b) && odm_time_a < odm_time_b;\n  };\n\n  for (auto b = begin(odm_journeys); b != end(odm_journeys);) {\n    auto is_dominated = false;\n    for (auto a = begin(odm_journeys); a != end(odm_journeys); ++a) {\n      if (a != b && pareto_dom(*a, *b)) {\n        is_dominated = true;\n        break;\n      }\n    }\n    if (is_dominated) {\n      b = odm_journeys.erase(b);\n    } else {\n      ++b;\n    }\n  }\n}\n\napi::plan_response meta_router::run() {\n  auto const init_start = std::chrono::steady_clock::now();\n  utl::verify(r_.tt_ != nullptr && r_.tags_ != nullptr,\n              \"mode=TRANSIT requires timetable to be loaded\");\n  auto prepare_stats = motis::ep::stats_map_t{};\n  auto const start_intvl = std::visit(\n      utl::overloaded{[](n::interval<n::unixtime_t> const i) { return i; },\n                      [](n::unixtime_t const t) {\n                        return n::interval<n::unixtime_t>{t, t};\n                      }},\n      start_time_.start_time_);\n  auto search_intvl =\n      n::interval<n::unixtime_t>{start_time_.extend_interval_earlier_\n                                     ? start_intvl.to_ - kSearchIntervalSize\n                                     : start_intvl.from_,\n                                 start_time_.extend_interval_later_\n                                     ? start_intvl.from_ + kSearchIntervalSize\n                                     : start_intvl.to_};\n  search_intvl.from_ = r_.tt_->external_interval().clamp(search_intvl.from_);\n  search_intvl.to_ = r_.tt_->external_interval().clamp(search_intvl.to_);\n  auto const context_intvl = n::interval<n::unixtime_t>{\n      search_intvl.from_ - kContextPadding, search_intvl.to_ + kContextPadding};\n  auto const taxi_intvl =\n      query_.arriveBy_\n          ? n::interval<n::unixtime_t>{context_intvl.from_ - kODMLookAhead,\n                                       context_intvl.to_}\n          : n::interval<n::unixtime_t>{context_intvl.from_,\n                                       context_intvl.to_ + kODMLookAhead};\n  auto const to_osr_loc = [&](auto const& place) {\n    return std::visit(\n        utl::overloaded{\n            [](osr::location const& l) { return l; },\n            [&](tt_location const& l) {\n              return osr::location{\n                  .pos_ = tt_->locations_.coordinates_[l.l_],\n                  .lvl_ = osr::level_t{std::uint8_t{osr::level_t::kNoLevel}}};\n            }},\n        place);\n  };\n  auto p = prima{r_.config_.prima_->url_, to_osr_loc(from_), to_osr_loc(to_),\n                 query_};\n  p.init(search_intvl, taxi_intvl, odm_pre_transit_, odm_post_transit_,\n         odm_direct_, ride_sharing_pre_transit_, ride_sharing_post_transit_,\n         ride_sharing_direct_, *tt_, rtt_, r_, e_, gbfs_rd_, from_place_,\n         to_place_, query_, start_time_, api_version_);\n\n  std::erase(start_modes_, api::ModeEnum::ODM);\n  std::erase(start_modes_, api::ModeEnum::RIDE_SHARING);\n  std::erase(dest_modes_, api::ModeEnum::ODM);\n  std::erase(dest_modes_, api::ModeEnum::RIDE_SHARING);\n\n  print_time(\n      init_start,\n      fmt::format(\"[init] (#first_mile_offsets: {}, #last_mile_offsets: {}, \"\n                  \"#direct_rides: {})\",\n                  p.first_mile_taxi_.size(), p.last_mile_taxi_.size(),\n                  p.direct_taxi_.size()),\n      r_.metrics_->routing_execution_duration_seconds_init_);\n\n  auto const blacklist_start = std::chrono::steady_clock::now();\n  auto const blacklisted_taxis = p.blacklist_taxi(*tt_, taxi_intvl);\n  print_time(blacklist_start,\n             fmt::format(\"[blacklist taxi] (#first_mile_offsets: {}, \"\n                         \"#last_mile_offsets: {}, #direct_rides: {})\",\n                         p.first_mile_taxi_.size(), p.last_mile_taxi_.size(),\n                         p.direct_taxi_.size()),\n             r_.metrics_->routing_execution_duration_seconds_blacklisting_);\n\n  auto const whitelist_ride_sharing_start = std::chrono::steady_clock::now();\n  auto const whitelisted_ride_sharing = p.whitelist_ride_sharing(*tt_);\n  n::log(n::log_lvl::debug, \"motis.prima\",\n         \"[whitelist ride-sharing] ride-sharing events after whitelisting: {}\",\n         p.n_ride_sharing_events());\n  print_time(\n      whitelist_ride_sharing_start,\n      fmt::format(\"[whitelist ride-sharing] (#first_mile_ride_sharing: {}, \"\n                  \"#last_mile_ride_sharing: {}, #direct_ride_sharing: {})\",\n                  p.first_mile_ride_sharing_.size(),\n                  p.last_mile_ride_sharing_.size(),\n                  p.direct_ride_sharing_.size()),\n      r_.metrics_->routing_execution_duration_seconds_blacklisting_);\n\n  auto const prep_queries_start = std::chrono::steady_clock::now();\n  auto const [first_mile_taxi_short, first_mile_taxi_long] =\n      get_td_offsets_split(p.first_mile_taxi_, p.first_mile_taxi_times_,\n                           kOdmTransportModeId);\n  auto const [last_mile_taxi_short, last_mile_taxi_long] = get_td_offsets_split(\n      p.last_mile_taxi_, p.last_mile_taxi_times_, kOdmTransportModeId);\n  auto const params = get_osr_parameters(query_);\n  auto const pre_transit_time = std::min(\n      std::chrono::seconds{query_.maxPreTransitTime_},\n      std::chrono::seconds{\n          r_.config_.get_limits().street_routing_max_prepost_transit_seconds_});\n  auto const post_transit_time = std::min(\n      std::chrono::seconds{query_.maxPostTransitTime_},\n      std::chrono::seconds{\n          r_.config_.get_limits().street_routing_max_prepost_transit_seconds_});\n  auto const qf = query_factory{\n      .base_query_ = get_base_query(context_intvl),\n      .start_walk_ = r_.get_offsets(\n          rtt_, start_,\n          query_.arriveBy_ ? osr::direction::kBackward\n                           : osr::direction::kForward,\n          start_modes_, start_form_factors_, start_propulsion_types_,\n          start_rental_providers_, start_rental_provider_groups_,\n          start_ignore_rental_return_constraints_, params,\n          query_.pedestrianProfile_, query_.elevationCosts_,\n          query_.arriveBy_ ? post_transit_time : pre_transit_time,\n          query_.maxMatchingDistance_, gbfs_rd_, prepare_stats),\n      .dest_walk_ = r_.get_offsets(\n          rtt_, dest_,\n          query_.arriveBy_ ? osr::direction::kForward\n                           : osr::direction::kBackward,\n          dest_modes_, dest_form_factors_, dest_propulsion_types_,\n          dest_rental_providers_, dest_rental_provider_groups_,\n          dest_ignore_rental_return_constraints_, params,\n          query_.pedestrianProfile_, query_.elevationCosts_,\n          query_.arriveBy_ ? pre_transit_time : post_transit_time,\n          query_.maxMatchingDistance_, gbfs_rd_, prepare_stats),\n      .td_start_walk_ = r_.get_td_offsets(\n          rtt_, e_, start_,\n          query_.arriveBy_ ? osr::direction::kBackward\n                           : osr::direction::kForward,\n          start_modes_, params, query_.pedestrianProfile_,\n          query_.elevationCosts_, query_.maxMatchingDistance_,\n          query_.arriveBy_ ? post_transit_time : pre_transit_time,\n          context_intvl, prepare_stats),\n      .td_dest_walk_ = r_.get_td_offsets(\n          rtt_, e_, dest_,\n          query_.arriveBy_ ? osr::direction::kForward\n                           : osr::direction::kBackward,\n          dest_modes_, params, query_.pedestrianProfile_,\n          query_.elevationCosts_, query_.maxMatchingDistance_,\n          query_.arriveBy_ ? pre_transit_time : post_transit_time,\n          context_intvl, prepare_stats),\n      .start_taxi_short_ =\n          query_.arriveBy_ ? last_mile_taxi_short : first_mile_taxi_short,\n      .start_taxi_long_ =\n          query_.arriveBy_ ? last_mile_taxi_long : first_mile_taxi_long,\n      .dest_taxi_short_ =\n          query_.arriveBy_ ? first_mile_taxi_short : last_mile_taxi_short,\n      .dest_taxi_long_ =\n          query_.arriveBy_ ? first_mile_taxi_long : last_mile_taxi_long,\n      .start_ride_sharing_ = query_.arriveBy_\n                                 ? get_td_offsets(p.last_mile_ride_sharing_,\n                                                  kRideSharingTransportModeId)\n                                 : get_td_offsets(p.first_mile_ride_sharing_,\n                                                  kRideSharingTransportModeId),\n      .dest_ride_sharing_ = query_.arriveBy_\n                                ? get_td_offsets(p.first_mile_ride_sharing_,\n                                                 kRideSharingTransportModeId)\n                                : get_td_offsets(p.last_mile_ride_sharing_,\n                                                 kRideSharingTransportModeId)};\n  print_time(prep_queries_start, \"[prepare queries]\",\n             r_.metrics_->routing_execution_duration_seconds_preparing_);\n\n  auto const routing_start = std::chrono::steady_clock::now();\n  auto sub_queries =\n      qf.make_queries(blacklisted_taxis, whitelisted_ride_sharing);\n  n::log(n::log_lvl::debug, \"motis.prima\",\n         \"[prepare queries] {} queries prepared\", sub_queries.size());\n  auto const results = search_interval(sub_queries);\n  utl::verify(!results.empty(), \"prima: public transport result expected\");\n  auto const& pt_result = results.front();\n  auto taxi_journeys = collect_odm_journeys(results, kOdmTransportModeId);\n  shorten(taxi_journeys, p.first_mile_taxi_, p.first_mile_taxi_times_,\n          p.last_mile_taxi_, p.last_mile_taxi_times_, *tt_, rtt_, query_);\n  auto ride_share_journeys =\n      collect_odm_journeys(results, kRideSharingTransportModeId);\n  fix_first_mile_duration(ride_share_journeys, p.first_mile_ride_sharing_,\n                          p.first_mile_ride_sharing_,\n                          kRideSharingTransportModeId);\n  fix_last_mile_duration(ride_share_journeys, p.last_mile_ride_sharing_,\n                         p.last_mile_ride_sharing_,\n                         kRideSharingTransportModeId);\n  utl::erase_duplicates(\n      taxi_journeys, std::less<n::routing::journey>{},\n      [](auto const& a, auto const& b) {\n        return a == b &&\n               odm_time(a.legs_.front()) == odm_time(b.legs_.front()) &&\n               odm_time(a.legs_.back()) == odm_time(b.legs_.back());\n      });\n  n::log(n::log_lvl::debug, \"motis.prima\", \"[routing] interval searched: {}\",\n         pt_result.interval_);\n  print_time(routing_start, \"[routing]\",\n             r_.metrics_->routing_execution_duration_seconds_routing_);\n\n  auto const whitelist_start = std::chrono::steady_clock::now();\n  auto const was_whitelist_response_valid =\n      p.whitelist_taxi(taxi_journeys, *tt_);\n  if (was_whitelist_response_valid) {\n    add_direct_odm(p.direct_taxi_, taxi_journeys, from_, to_, query_.arriveBy_,\n                   kOdmTransportModeId);\n  }\n  if (whitelisted_ride_sharing) {\n    add_direct_odm(p.direct_ride_sharing_, ride_share_journeys, from_, to_,\n                   query_.arriveBy_, kRideSharingTransportModeId);\n  }\n  print_time(whitelist_start,\n             fmt::format(\"[whitelisting] (#first_mile_taxi: {}, \"\n                         \"#last_mile_taxi: {}, #direct_taxi: {})\",\n                         p.first_mile_taxi_.size(), p.last_mile_taxi_.size(),\n                         p.direct_taxi_.size()),\n             r_.metrics_->routing_execution_duration_seconds_whitelisting_);\n  r_.metrics_->routing_odm_journeys_found_whitelist_.Observe(\n      static_cast<double>(taxi_journeys.size()));\n\n  pareto_dominance(taxi_journeys);\n  taxi_journeys.insert(end(taxi_journeys), begin(pt_result.journeys_),\n                       end(pt_result.journeys_));\n  taxi_journeys.insert(end(taxi_journeys), begin(ride_share_journeys),\n                       end(ride_share_journeys));\n  utl::sort(taxi_journeys, [](auto const& a, auto const& b) {\n    return std::tuple{a.departure_time(), a.arrival_time(), a.transfers_} <\n           std::tuple{b.departure_time(), b.arrival_time(), b.transfers_};\n  });\n\n  r_.metrics_->routing_journeys_found_.Increment(\n      static_cast<double>(taxi_journeys.size()));\n  r_.metrics_->routing_execution_duration_seconds_total_.Observe(\n      static_cast<double>(std::chrono::duration_cast<std::chrono::milliseconds>(\n                              std::chrono::steady_clock::now() - init_start)\n                              .count()) /\n      1000.0);\n\n  if (!taxi_journeys.empty()) {\n    r_.metrics_->routing_journey_duration_seconds_.Observe(static_cast<double>(\n        to_seconds(taxi_journeys.begin()->arrival_time() -\n                   taxi_journeys.begin()->departure_time())));\n  }\n  return {\n      .from_ = from_place_,\n      .to_ = to_place_,\n      .direct_ = std::move(direct_),\n      .itineraries_ = utl::to_vec(\n          taxi_journeys,\n          [&, cache = street_routing_cache_t{}](auto&& j) mutable {\n            if (ep::blocked.get() == nullptr && r_.w_ != nullptr) {\n              ep::blocked.reset(\n                  new osr::bitvec<osr::node_idx_t>{r_.w_->n_nodes()});\n            }\n            auto const detailed_transfers =\n                query_.detailedTransfers_.value_or(query_.detailedLegs_);\n            auto response = journey_to_response(\n                r_.w_, r_.l_, r_.pl_, *tt_, *r_.tags_, r_.fa_, e_, rtt_,\n                r_.matches_, r_.elevations_, r_.shapes_, gbfs_rd_, r_.ae_,\n                r_.tz_, j, start_, dest_, cache, ep::blocked.get(),\n                query_.requireCarTransport_ && query_.useRoutedTransfers_,\n                params, query_.pedestrianProfile_, query_.elevationCosts_,\n                query_.joinInterlinedLegs_, detailed_transfers,\n                query_.detailedLegs_, query_.withFares_,\n                query_.withScheduledSkippedStops_,\n                r_.config_.timetable_.value().max_matching_distance_,\n                query_.maxMatchingDistance_, api_version_,\n                query_.ignorePreTransitRentalReturnConstraints_,\n                query_.ignorePostTransitRentalReturnConstraints_,\n                query_.language_);\n\n            if (response.legs_.front().mode_ == api::ModeEnum::RIDE_SHARING &&\n                response.legs_.size() == 1) {\n              for (auto const [i, a] : utl::enumerate(p.direct_ride_sharing_)) {\n                if (a.dep_ == response.legs_.front().startTime_ &&\n                    a.arr_ == response.legs_.front().endTime_) {\n                  response.legs_.front().tripId_ = std::optional{\n                      p.direct_ride_sharing_tour_ids_.at(i).view()};\n                  break;\n                }\n              }\n              return response;\n            }\n            if (response.legs_.front().mode_ == api::ModeEnum::RIDE_SHARING) {\n              for (auto const [i, a] :\n                   utl::enumerate(p.first_mile_ride_sharing_)) {\n                if (a.time_at_start_ ==\n                        response.legs_.front()\n                            .startTime_ &&  // not looking at time_at_stop_\n                                            // because we would again need to\n                                            // take into account the 5 min\n                                            // shift...\n                    r_.tags_->id(*tt_, a.stop_) ==\n                        response.legs_.front().to_.stopId_) {\n                  response.legs_.front().tripId_ = std::optional{\n                      p.first_mile_ride_sharing_tour_ids_.at(i).view()};\n                  break;\n                }\n              }\n            }\n            if (response.legs_.back().mode_ == api::ModeEnum::RIDE_SHARING) {\n              for (auto const [i, a] :\n                   utl::enumerate(p.last_mile_ride_sharing_)) {\n                if (a.time_at_start_ ==\n                        response.legs_.back()\n                            .endTime_ &&  // not looking at time_at_stop_\n                                          // because we would again need to take\n                                          // into account the 5 min shift...\n                    r_.tags_->id(*tt_, a.stop_) ==\n                        response.legs_.back().from_.stopId_) {\n                  response.legs_.back().tripId_ = std::optional{\n                      p.last_mile_ride_sharing_tour_ids_.at(i).view()};\n                  break;\n                }\n              }\n            }\n\n            auto const match_times = [&](motis::api::Leg const& leg,\n                                         boost::json::array const& entries)\n                -> std::optional<std::string> {\n              auto const it = std::find_if(\n                  std::begin(entries), std::end(entries),\n                  [&](boost::json::value const& json_entry) {\n                    if (json_entry.is_null()) {\n                      return false;\n                    }\n                    auto const& object_entry = json_entry.as_object();\n                    return to_unix(object_entry.at(\"pickupTime\").as_int64()) ==\n                               leg.startTime_ &&\n                           to_unix(object_entry.at(\"dropoffTime\").as_int64()) ==\n                               leg.endTime_;\n                  });\n\n              if (it != std::end(entries)) {\n                return boost::json::serialize(it->as_object());\n              }\n\n              return std::nullopt;\n            };\n\n            auto const match_location =\n                [&](motis::api::Leg const& leg, boost::json::array const& outer,\n                    std::vector<nigiri::location_idx_t> const& locations,\n                    bool const check_to) -> std::optional<std::string> {\n              auto const& stop_id =\n                  check_to ? leg.to_.stopId_ : leg.from_.stopId_;\n              for (auto const [loc, outer_value] : utl::zip(locations, outer)) {\n                if (stop_id != r_.tags_->id(*tt_, loc)) {\n                  continue;\n                }\n                auto const& inner = outer_value.as_array();\n                if (auto result = match_times(leg, inner)) {\n                  return result;\n                }\n              }\n              return std::nullopt;\n            };\n\n            if (!was_whitelist_response_valid) {\n              return response;\n            }\n            if (response.legs_.size() == 1 &&\n                response.legs_.front().mode_ == api::ModeEnum::ODM) {\n              if (auto const id = match_times(\n                      response.legs_.front(),\n                      p.whitelist_response_.at(\"direct\").as_array());\n                  id.has_value()) {\n                response.legs_.front().tripId_ = std::optional{*id};\n              }\n              return response;\n            }\n            if (!response.legs_.empty() &&\n                response.legs_.front().mode_ == api::ModeEnum::ODM) {\n              if (auto const id = match_location(\n                      response.legs_.front(),\n                      p.whitelist_response_.at(\"start\").as_array(),\n                      p.whitelist_first_mile_locations_, true);\n                  id.has_value()) {\n                response.legs_.front().tripId_ = std::optional{*id};\n              }\n            }\n            if (!response.legs_.empty() &&\n                response.legs_.back().mode_ == api::ModeEnum::ODM) {\n              if (auto const id = match_location(\n                      response.legs_.back(),\n                      p.whitelist_response_.at(\"target\").as_array(),\n                      p.whitelist_last_mile_locations_, false);\n                  id.has_value()) {\n                response.legs_.back().tripId_ = std::optional{*id};\n              }\n            }\n            return response;\n          }),\n      .previousPageCursor_ =\n          fmt::format(\"EARLIER|{}\", to_seconds(search_intvl.from_)),\n      .nextPageCursor_ = fmt::format(\"LATER|{}\", to_seconds(search_intvl.to_))};\n}\n\n}  // namespace motis::odm\n"
  },
  {
    "path": "src/odm/odm.cc",
    "content": "#include \"motis/odm/odm.h\"\n\n#include <ranges>\n\n#include \"nigiri/routing/journey.h\"\n\n#include \"motis/transport_mode_ids.h\"\n\nnamespace motis::odm {\n\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\n\nbool by_stop(nr::start const& a, nr::start const& b) {\n  return std::tie(a.stop_, a.time_at_start_, a.time_at_stop_) <\n         std::tie(b.stop_, b.time_at_start_, b.time_at_stop_);\n}\n\nbool is_odm_leg(nr::journey::leg const& l,\n                nigiri::transport_mode_id_t const mode) {\n  return std::holds_alternative<nr::offset>(l.uses_) &&\n         std::get<nr::offset>(l.uses_).transport_mode_id_ == mode;\n}\n\nbool uses_odm(nr::journey const& j, nigiri::transport_mode_id_t const mode) {\n  return utl::any_of(j.legs_,\n                     [&](auto const& l) { return is_odm_leg(l, mode); });\n}\n\nbool is_pure_pt(nr::journey const& j) {\n  return !uses_odm(j, kOdmTransportModeId) &&\n         !uses_odm(j, kRideSharingTransportModeId);\n};\n\nn::duration_t odm_time(nr::journey::leg const& l) {\n  return is_odm_leg(l, kOdmTransportModeId) ||\n                 is_odm_leg(l, kRideSharingTransportModeId)\n             ? std::get<nr::offset>(l.uses_).duration()\n             : n::duration_t{0};\n}\n\nn::duration_t odm_time(nr::journey const& j) {\n  return std::transform_reduce(begin(j.legs_), end(j.legs_), n::duration_t{0},\n                               std::plus{},\n                               [](auto const& l) { return odm_time(l); });\n}\n\nn::duration_t pt_time(nr::journey const& j) {\n  return j.travel_time() - odm_time(j);\n}\n\nbool is_direct_odm(nr::journey const& j) {\n  return j.travel_time() == odm_time(j);\n}\n\nn::duration_t duration(nr::start const& ride) {\n  return std::chrono::abs(ride.time_at_stop_ - ride.time_at_start_);\n}\n\nstd::string odm_label(nr::journey const& j) {\n  return fmt::format(\n      \"[dep: {}, arr: {}, transfers: {}, start_odm: {}, dest_odm: {}]\",\n      j.start_time_, j.dest_time_, j.transfers_, odm_time(j.legs_.front()),\n      odm_time(j.legs_.back()));\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/prima.cc",
    "content": "#include \"motis/odm/prima.h\"\n\n#include <variant>\n\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/json.hpp\"\n\n#include \"utl/erase_if.h\"\n#include \"utl/pipes.h\"\n#include \"utl/zip.h\"\n\n#include \"nigiri/common/parse_time.h\"\n#include \"nigiri/logging.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/elevators/elevators.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/http_req.h\"\n#include \"motis/odm/bounds.h\"\n#include \"motis/odm/odm.h\"\n#include \"motis/transport_mode_ids.h\"\n\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\nnamespace json = boost::json;\n\nnamespace motis::odm {\n\nprima::prima(std::string const& prima_url,\n             osr::location const& from,\n             osr::location const& to,\n             api::plan_params const& query)\n    : query_{query},\n      taxi_blacklist_{prima_url + kBlacklistPath},\n      taxi_whitelist_{prima_url + kWhitelistPath},\n      ride_sharing_whitelist_{prima_url + kRidesharingPath},\n      from_{from},\n      to_{to},\n      fixed_{query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep},\n      cap_{\n          .wheelchairs_ = static_cast<std::uint8_t>(\n              query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR\n                  ? 1U\n                  : 0U),\n          .bikes_ =\n              static_cast<std::uint8_t>(query.requireBikeTransport_ ? 1 : 0),\n          .passengers_ = query.passengers_.value_or(1U),\n          .luggage_ = query.luggage_.value_or(0U)} {}\n\nn::duration_t init_direct(std::vector<direct_ride>& rides,\n                          ep::routing const& r,\n                          elevators const* e,\n                          gbfs::gbfs_routing_data& gbfs,\n                          api::Place const& from_p,\n                          api::Place const& to_p,\n                          n::interval<n::unixtime_t> const intvl,\n                          api::plan_params const& query,\n                          unsigned api_version) {\n  auto [_, direct_duration] = r.route_direct(\n      e, gbfs, {}, from_p, to_p, {api::ModeEnum::CAR}, std::nullopt,\n      std::nullopt, std::nullopt, std::nullopt, false, intvl.from_, false,\n      get_osr_parameters(query), query.pedestrianProfile_,\n      query.elevationCosts_, kODMMaxDuration, query.maxMatchingDistance_,\n      kODMDirectFactor, query.detailedLegs_, api_version);\n\n  auto const step =\n      std::chrono::duration_cast<n::unixtime_t::duration>(kODMDirectPeriod);\n  if (direct_duration < kODMMaxDuration) {\n    if (query.arriveBy_) {\n      auto const base_time = intvl.to_ - direct_duration;\n      auto const midnight = std::chrono::floor<std::chrono::days>(base_time);\n      auto const mins_since_midnight =\n          std::chrono::duration_cast<std::chrono::minutes>(base_time -\n                                                           midnight);\n      auto const floored_5_min = (mins_since_midnight.count() / 5) * 5;\n      auto const start_time = midnight + std::chrono::minutes(floored_5_min);\n      for (auto arr = start_time; intvl.contains(arr); arr -= step) {\n        rides.push_back({.dep_ = arr - direct_duration, .arr_ = arr});\n      }\n    } else {\n      auto const base_start = intvl.from_;\n      auto const midnight_start =\n          std::chrono::floor<std::chrono::days>(base_start);\n      auto const mins_since_midnight_start =\n          std::chrono::duration_cast<std::chrono::minutes>(base_start -\n                                                           midnight_start);\n      auto const ceiled_5_min_start =\n          ((mins_since_midnight_start.count() + 4) / 5) * 5;\n      auto const start_time_for_depart =\n          midnight_start + std::chrono::minutes(ceiled_5_min_start);\n      for (auto dep = start_time_for_depart; intvl.contains(dep); dep += step) {\n        rides.push_back({.dep_ = dep, .arr_ = dep + direct_duration});\n      }\n    }\n  }\n\n  return direct_duration;\n}\n\nvoid init_pt(std::vector<n::routing::offset>& offsets,\n             std::vector<n::routing::start>& rides,\n             ep::routing const& r,\n             osr::location const& l,\n             osr::direction dir,\n             api::plan_params const& query,\n             gbfs::gbfs_routing_data& gbfs_rd,\n             n::timetable const& tt,\n             n::rt_timetable const* rtt,\n             n::interval<n::unixtime_t> const& intvl,\n             n::routing::query const& start_time,\n             n::routing::location_match_mode location_match_mode,\n             std::chrono::seconds const max) {\n  auto stats = std::map<std::string, std::uint64_t>{};\n  offsets = r.get_offsets(rtt, l, dir, {api::ModeEnum::CAR}, std::nullopt,\n                          std::nullopt, std::nullopt, std::nullopt, false,\n                          get_osr_parameters(query), query.pedestrianProfile_,\n                          query.elevationCosts_, max,\n                          query.maxMatchingDistance_, gbfs_rd, stats);\n\n  std::erase_if(offsets, [&](n::routing::offset const& o) {\n    return r.ride_sharing_bounds_ != nullptr &&\n           !r.ride_sharing_bounds_->contains(\n               r.tt_->locations_.coordinates_[o.target_]);\n  });\n\n  for (auto& o : offsets) {\n    o.duration_ += kODMTransferBuffer;\n  }\n\n  rides.reserve(offsets.size() * 2);\n\n  n::routing::get_starts(\n      dir == osr::direction::kForward ? n::direction::kForward\n                                      : n::direction::kBackward,\n      tt, rtt, intvl, offsets, {}, {}, n::routing::kMaxTravelTime,\n      location_match_mode, false, rides, true, start_time.prf_idx_,\n      start_time.transfer_time_settings_);\n}\n\nvoid prima::init(n::interval<n::unixtime_t> const& search_intvl,\n                 n::interval<n::unixtime_t> const& taxi_intvl,\n                 bool use_first_mile_taxi,\n                 bool use_last_mile_taxi,\n                 bool use_direct_taxi,\n                 bool use_first_mile_ride_sharing,\n                 bool use_last_mile_ride_sharing,\n                 bool use_direct_ride_sharing,\n                 n::timetable const& tt,\n                 n::rt_timetable const* rtt,\n                 ep::routing const& r,\n                 elevators const* e,\n                 gbfs::gbfs_routing_data& gbfs,\n                 api::Place const& from,\n                 api::Place const& to,\n                 api::plan_params const& query,\n                 n::routing::query const& n_query,\n                 unsigned api_version) {\n  direct_duration_ = std::optional<std::chrono::minutes>{};\n  if ((use_direct_ride_sharing || use_direct_taxi) && r.w_ && r.l_ &&\n      (r.ride_sharing_bounds_ == nullptr ||\n       (r.ride_sharing_bounds_->contains(from_.pos_) &&\n        r.ride_sharing_bounds_->contains(to_.pos_)))) {\n    direct_duration_ = init_direct(direct_ride_sharing_, r, e, gbfs, from, to,\n                                   search_intvl, query, api_version);\n\n    if (use_direct_taxi && r.odm_bounds_ != nullptr &&\n        r.odm_bounds_->contains(from_.pos_) &&\n        r.odm_bounds_->contains(to_.pos_)) {\n      direct_taxi_ = direct_ride_sharing_;\n    }\n\n    if (!use_direct_ride_sharing) {\n      direct_ride_sharing_.clear();\n    }\n  }\n\n  auto const max_offset_duration =\n      direct_duration_\n          ? std::min(std::max(*direct_duration_, kODMOffsetMinImprovement) -\n                         kODMOffsetMinImprovement,\n                     kODMMaxDuration)\n          : kODMMaxDuration;\n\n  if (use_first_mile_ride_sharing || use_first_mile_taxi) {\n    init_pt(\n        first_mile_taxi_, first_mile_ride_sharing_, r, from_,\n        osr::direction::kForward, query, gbfs, tt, rtt, taxi_intvl, n_query,\n        query.arriveBy_ ? n_query.dest_match_mode_ : n_query.start_match_mode_,\n        max_offset_duration);\n\n    if (!use_first_mile_taxi || r.odm_bounds_ == nullptr ||\n        !r.odm_bounds_->contains(from_.pos_)) {\n      first_mile_taxi_.clear();\n    } else {\n      std::erase_if(first_mile_taxi_, [&](n::routing::offset const& o) {\n        return !r.odm_bounds_->contains(\n            r.tt_->locations_.coordinates_[o.target_]);\n      });\n    }\n\n    if (!use_first_mile_ride_sharing) {\n      first_mile_ride_sharing_.clear();\n    }\n  }\n\n  if (use_last_mile_ride_sharing || use_last_mile_taxi) {\n    init_pt(\n        last_mile_taxi_, last_mile_ride_sharing_, r, to_,\n        osr::direction::kBackward, query, gbfs, tt, rtt, taxi_intvl, n_query,\n        query.arriveBy_ ? n_query.start_match_mode_ : n_query.dest_match_mode_,\n        max_offset_duration);\n\n    if (!use_last_mile_taxi || r.odm_bounds_ == nullptr ||\n        !r.odm_bounds_->contains(to_.pos_)) {\n      last_mile_taxi_.clear();\n    } else {\n      std::erase_if(last_mile_taxi_, [&](n::routing::offset const& o) {\n        return !r.odm_bounds_->contains(\n            r.tt_->locations_.coordinates_[o.target_]);\n      });\n    }\n\n    if (!use_last_mile_ride_sharing) {\n      last_mile_ride_sharing_.clear();\n    }\n  }\n\n  auto const by_duration = [](auto const& a, auto const& b) {\n    return a.duration_ < b.duration_;\n  };\n  utl::sort(first_mile_taxi_, by_duration);\n  utl::sort(last_mile_taxi_, by_duration);\n}\n\nstd::int64_t to_millis(n::unixtime_t const t) {\n  return std::chrono::duration_cast<std::chrono::milliseconds>(\n             t.time_since_epoch())\n      .count();\n}\n\nn::unixtime_t to_unix(std::int64_t const t) {\n  return n::unixtime_t{\n      std::chrono::duration_cast<n::i32_minutes>(std::chrono::milliseconds{t})};\n}\n\njson::array to_json(std::vector<n::routing::start> const& rides,\n                    n::timetable const& tt,\n                    which_mile const wm) {\n  auto a = json::array{};\n  utl::equal_ranges_linear(\n      rides,\n      [](n::routing::start const& a, n::routing::start const& b) {\n        return a.stop_ == b.stop_;\n      },\n      [&](auto&& from_it, auto&& to_it) {\n        auto const& pos = tt.locations_.coordinates_[from_it->stop_];\n        a.emplace_back(json::value{\n            {\"lat\", pos.lat_},\n            {\"lng\", pos.lng_},\n            {\"times\",\n             utl::all(from_it, to_it) |\n                 utl::transform([&](n::routing::start const& s) {\n                   return wm == kFirstMile\n                              ? to_millis(s.time_at_stop_ - kODMTransferBuffer)\n                              : to_millis(s.time_at_stop_ + kODMTransferBuffer);\n                 }) |\n                 utl::emplace_back_to<json::array>()}});\n      });\n  return a;\n}\n\njson::array to_json(std::vector<direct_ride> const& v,\n                    n::event_type const fixed) {\n  return utl::all(v)  //\n         | utl::transform([&](direct_ride const& r) {\n             return to_millis(fixed == n::event_type::kDep ? r.dep_ : r.arr_);\n           })  //\n         | utl::emplace_back_to<json::array>();\n}\n\nvoid tag_invoke(json::value_from_tag const&,\n                json::value& jv,\n                capacities const& c) {\n  jv = {{\"wheelchairs\", c.wheelchairs_},\n        {\"bikes\", c.bikes_},\n        {\"passengers\", c.passengers_},\n        {\"luggage\", c.luggage_}};\n}\n\nstd::string make_whitelist_request(\n    osr::location const& from,\n    osr::location const& to,\n    std::vector<n::routing::start> const& first_mile,\n    std::vector<n::routing::start> const& last_mile,\n    std::vector<direct_ride> const& direct,\n    n::event_type const fixed,\n    capacities const& cap,\n    n::timetable const& tt) {\n  return json::serialize(\n      json::value{{\"start\", {{\"lat\", from.pos_.lat_}, {\"lng\", from.pos_.lng_}}},\n                  {\"target\", {{\"lat\", to.pos_.lat_}, {\"lng\", to.pos_.lng_}}},\n                  {\"startBusStops\", to_json(first_mile, tt, kFirstMile)},\n                  {\"targetBusStops\", to_json(last_mile, tt, kLastMile)},\n                  {\"directTimes\", to_json(direct, fixed)},\n                  {\"startFixed\", fixed == n::event_type::kDep},\n                  {\"capacities\", json::value_from(cap)}});\n}\n\nstd::size_t prima::n_ride_sharing_events() const {\n  return first_mile_ride_sharing_.size() + last_mile_ride_sharing_.size() +\n         direct_ride_sharing_.size();\n}\n\nstd::size_t n_rides_in_response(json::array const& ja) {\n  return std::accumulate(\n      ja.begin(), ja.end(), std::size_t{0U},\n      [](auto const& a, auto const& b) { return a + b.as_array().size(); });\n}\n\nvoid fix_first_mile_duration(std::vector<nr::journey>& journeys,\n                             std::vector<nr::start> const& first_mile,\n                             std::vector<nr::start> const& prev_first_mile,\n                             n::transport_mode_id_t const mode) {\n  for (auto const [curr, prev] : utl::zip(first_mile, prev_first_mile)) {\n\n    auto const uses_prev = [&,\n                            prev2 = prev /* hack for MacOS - fixed with 16 */](\n                               n::routing::journey const& j) {\n      return j.legs_.size() > 1 &&\n             j.legs_.front().dep_time_ == prev2.time_at_start_ &&\n             j.legs_.front().arr_time_ >= prev2.time_at_stop_ &&\n             (j.legs_.front().arr_time_ == prev2.time_at_stop_ ||\n              mode == kRideSharingTransportModeId) &&\n             j.legs_.front().to_ == prev2.stop_ &&\n             is_odm_leg(j.legs_.front(), mode);\n    };\n\n    if (curr.time_at_start_ == kInfeasible) {\n      utl::erase_if(journeys, uses_prev);\n    } else {\n      for (auto& j : journeys) {\n        if (uses_prev(j)) {\n          auto const l = begin(j.legs_);\n          if (std::holds_alternative<n::footpath>(std::next(l)->uses_)) {\n            continue;  // odm leg fixed already before with a different\n                       // time_at_stop (rideshare)\n          }\n          l->dep_time_ = curr.time_at_start_;\n          l->arr_time_ =\n              curr.time_at_stop_ - (mode == kRideSharingTransportModeId\n                                        ? kODMTransferBuffer\n                                        : n::duration_t{0});\n          std::get<n::routing::offset>(l->uses_).duration_ =\n              l->arr_time_ - l->dep_time_;\n          // fill gap (transfer/waiting) with footpath\n          j.legs_.emplace(\n              std::next(l), n::direction::kForward, l->to_, l->to_,\n              l->arr_time_, std::next(l)->dep_time_,\n              n::footpath{l->to_, std::next(l)->dep_time_ - l->arr_time_});\n        }\n      }\n    }\n  }\n};\n\nvoid fix_last_mile_duration(std::vector<nr::journey>& journeys,\n                            std::vector<nr::start> const& last_mile,\n                            std::vector<nr::start> const& prev_last_mile,\n                            n::transport_mode_id_t const mode) {\n  for (auto const [curr, prev] : utl::zip(last_mile, prev_last_mile)) {\n    auto const uses_prev =\n        [&, prev2 = prev /* hack for MacOS - fixed with 16 */](auto const& j) {\n          return j.legs_.size() > 1 &&\n                 j.legs_.back().dep_time_ <= prev2.time_at_stop_ &&\n                 (j.legs_.back().dep_time_ == prev2.time_at_stop_ ||\n                  mode == kRideSharingTransportModeId) &&\n                 j.legs_.back().arr_time_ == prev2.time_at_start_ &&\n                 j.legs_.back().from_ == prev2.stop_ &&\n                 is_odm_leg(j.legs_.back(), mode);\n        };\n\n    if (curr.time_at_start_ == kInfeasible) {\n      utl::erase_if(journeys, uses_prev);\n    } else {\n      for (auto& j : journeys) {\n        if (uses_prev(j)) {\n          auto const l = std::prev(end(j.legs_));\n          if (std::holds_alternative<n::footpath>(std::prev(l)->uses_)) {\n            continue;  // odm leg fixed already before with a different\n                       // time_at_stop (rideshare)\n          }\n          l->dep_time_ =\n              curr.time_at_stop_ + (mode == kRideSharingTransportModeId\n                                        ? kODMTransferBuffer\n                                        : n::duration_t{0});\n          l->arr_time_ = curr.time_at_start_;\n          std::get<n::routing::offset>(l->uses_).duration_ =\n              l->arr_time_ - l->dep_time_;\n          // fill gap (transfer/waiting) with footpath\n          j.legs_.emplace(\n              l, n::direction::kForward, l->from_, l->from_,\n              std::prev(l)->arr_time_, l->dep_time_,\n              n::footpath{l->from_, l->dep_time_ - std::prev(l)->arr_time_});\n        }\n      }\n    }\n  }\n};\n\nvoid add_direct_odm(std::vector<direct_ride> const& direct,\n                    std::vector<nr::journey>& odm_journeys,\n                    place_t const& from,\n                    place_t const& to,\n                    bool arrive_by,\n                    n::transport_mode_id_t const mode) {\n  auto from_l = std::visit(\n      utl::overloaded{[](osr::location const&) {\n                        return get_special_station(n::special_station::kStart);\n                      },\n                      [](tt_location const& tt_l) { return tt_l.l_; }},\n      from);\n  auto to_l = std::visit(\n      utl::overloaded{[](osr::location const&) {\n                        return get_special_station(n::special_station::kEnd);\n                      },\n                      [](tt_location const& tt_l) { return tt_l.l_; }},\n      to);\n\n  if (arrive_by) {\n    std::swap(from_l, to_l);\n  }\n\n  for (auto const& d : direct) {\n    odm_journeys.push_back(n::routing::journey{\n        .legs_ = {{n::direction::kForward, from_l, to_l, d.dep_, d.arr_,\n                   n::routing::offset{to_l, std::chrono::abs(d.arr_ - d.dep_),\n                                      mode}}},\n        .start_time_ = d.dep_,\n        .dest_time_ = d.arr_,\n        .dest_ = to_l,\n        .transfers_ = 0U});\n  }\n  n::log(n::log_lvl::debug, \"motis.prima\",\n         \"[whitelist] added {} direct rides for mode {}\", direct.size(), mode);\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/query_factory.cc",
    "content": "#include \"motis/odm/query_factory.h\"\n\n#include \"motis/endpoints/routing.h\"\n\nnamespace motis::odm {\n\nnamespace n = nigiri;\n\nstd::vector<n::routing::query> query_factory::make_queries(\n    bool const with_taxi, bool const with_ride_sharing) const {\n  auto queries = std::vector<n::routing::query>{};\n  queries.push_back(\n      make(start_walk_, td_start_walk_, dest_walk_, td_dest_walk_));\n  if (with_taxi) {\n    if (!dest_taxi_short_.empty()) {\n      queries.push_back(\n          make(start_walk_, td_start_walk_, dest_walk_, dest_taxi_short_));\n    }\n    if (!dest_taxi_long_.empty()) {\n      queries.push_back(\n          make(start_walk_, td_start_walk_, dest_walk_, dest_taxi_long_));\n    }\n    if (!start_taxi_short_.empty()) {\n      queries.push_back(\n          make(start_walk_, start_taxi_short_, dest_walk_, td_dest_walk_));\n    }\n    if (!start_taxi_long_.empty()) {\n      queries.push_back(\n          make(start_walk_, start_taxi_long_, dest_walk_, td_dest_walk_));\n    }\n  }\n  if (with_ride_sharing) {\n    if (!start_ride_sharing_.empty()) {\n      queries.push_back(\n          make(start_walk_, start_ride_sharing_, dest_walk_, td_dest_walk_));\n    }\n    if (!dest_ride_sharing_.empty()) {\n      queries.push_back(\n          make(start_walk_, td_start_walk_, dest_walk_, dest_ride_sharing_));\n    }\n  }\n  return queries;\n}\n\nn::routing::query query_factory::make(\n    std::vector<n::routing::offset> const& start,\n    n::hash_map<n::location_idx_t, std::vector<n::routing::td_offset>> const&\n        td_start,\n    std::vector<n::routing::offset> const& dest,\n    n::hash_map<n::location_idx_t, std::vector<n::routing::td_offset>> const&\n        td_dest) const {\n  auto q = base_query_;\n  q.start_ = start;\n  q.destination_ = dest;\n  q.td_start_ = td_start;\n  q.td_dest_ = td_dest;\n  motis::ep::remove_slower_than_fastest_direct(q);\n  return q;\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/shorten.cc",
    "content": "#include \"motis/odm/odm.h\"\n\n#include \"nigiri/for_each_meta.h\"\n#include \"nigiri/logging.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/odm/prima.h\"\n#include \"motis/transport_mode_ids.h\"\n\nusing namespace std::chrono_literals;\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\n\nnamespace motis::odm {\n\nvoid shorten(std::vector<nr::journey>& odm_journeys,\n             std::vector<nr::offset> const& first_mile_taxi,\n             std::vector<service_times_t> const& first_mile_taxi_times,\n             std::vector<nr::offset> const& last_mile_taxi,\n             std::vector<service_times_t> const& last_mile_taxi_times,\n             n::timetable const& tt,\n             n::rt_timetable const* rtt,\n             api::plan_params const& query) {\n\n  auto const shorten_first_leg = [&](nr::journey& j) {\n    auto& odm_leg = begin(j.legs_)[0];\n    auto& pt_leg = begin(j.legs_)[1];\n\n    if (!is_odm_leg(odm_leg, kOdmTransportModeId) ||\n        !std::holds_alternative<nr::journey::run_enter_exit>(pt_leg.uses_)) {\n      return;\n    }\n\n    auto& ree = std::get<nr::journey::run_enter_exit>(pt_leg.uses_);\n    auto run = n::rt::frun(tt, rtt, ree.r_);\n    run.stop_range_.to_ = ree.stop_range_.to_ - 1U;\n    auto min_stop_idx = ree.stop_range_.from_;\n    auto min_odm_duration = odm_time(odm_leg);\n    auto shorter_ride = std::optional<nr::start>{};\n    for (auto const stop : run) {\n      if (stop.is_cancelled() ||\n          !stop.in_allowed(query.pedestrianProfile_ ==\n                           api::PedestrianProfileEnum::WHEELCHAIR) ||\n          (query.requireBikeTransport_ &&\n           !stop.bikes_allowed(n::event_type::kDep)) ||\n          (query.requireCarTransport_ &&\n           !stop.cars_allowed(n::event_type::kDep))) {\n        continue;\n      }\n      for (auto const [offset, times] :\n           utl::zip(first_mile_taxi, first_mile_taxi_times)) {\n        if (nr::matches(tt, nr::location_match_mode::kExact, offset.target_,\n                        stop.get_location_idx()) &&\n            utl::any_of(times, [&](auto const& t) {\n              return t.contains(stop.time(n::event_type::kDep) -\n                                offset.duration_) &&\n                     t.contains(stop.time(n::event_type::kDep) - 1min);\n            })) {\n          if (offset.duration_ < min_odm_duration) {\n            min_stop_idx = stop.stop_idx_;\n            min_odm_duration = offset.duration_;\n            shorter_ride = {.time_at_start_ = stop.time(n::event_type::kDep) -\n                                              offset.duration_,\n                            .time_at_stop_ = stop.time(n::event_type::kDep),\n                            .stop_ = offset.target_};\n          }\n          break;\n        }\n      }\n    }\n    if (shorter_ride) {\n      auto& odm_offset = std::get<nr::offset>(odm_leg.uses_);\n\n      auto const old_stop = odm_leg.to_;\n      auto const old_odm_time = std::chrono::minutes{odm_offset.duration_};\n      auto const old_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_;\n\n      j.start_time_ = odm_leg.dep_time_ = shorter_ride->time_at_start_;\n      odm_offset.duration_ = min_odm_duration;\n      odm_leg.arr_time_ = pt_leg.dep_time_ = shorter_ride->time_at_stop_;\n      odm_leg.to_ = odm_offset.target_ = pt_leg.from_ = shorter_ride->stop_;\n      ree.stop_range_.from_ = min_stop_idx;\n\n      auto const new_stop = odm_leg.to_;\n      auto const new_odm_time = std::chrono::minutes{odm_offset.duration_};\n      auto const new_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_;\n\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"shorten first leg: [stop: {}, ODM: {}, PT: {}] -> [stop: {}, \"\n             \"ODM: {}, PT: {}] (ODM: -{}, PT: +{})\",\n             n::loc{tt, old_stop}, old_odm_time, old_pt_time,\n             n::loc{tt, new_stop}, new_odm_time, new_pt_time,\n             std::chrono::minutes{old_odm_time - new_odm_time},\n             new_pt_time - old_pt_time);\n    }\n  };\n\n  auto const shorten_last_leg = [&](nr::journey& j) {\n    auto& odm_leg = rbegin(j.legs_)[0];\n    auto& pt_leg = rbegin(j.legs_)[1];\n\n    if (!is_odm_leg(odm_leg, kOdmTransportModeId) ||\n        !std::holds_alternative<nr::journey::run_enter_exit>(pt_leg.uses_)) {\n      return;\n    }\n\n    auto& ree = std::get<nr::journey::run_enter_exit>(pt_leg.uses_);\n    auto run = n::rt::frun(tt, rtt, ree.r_);\n    run.stop_range_.from_ = ree.stop_range_.from_ + 1U;\n    auto min_stop_idx = static_cast<n::stop_idx_t>(ree.stop_range_.to_ - 1U);\n    auto min_odm_duration = odm_time(odm_leg);\n    auto shorter_ride = std::optional<nr::start>{};\n    for (auto const stop : run) {\n      if (stop.is_cancelled() ||\n          !stop.out_allowed(query.pedestrianProfile_ ==\n                            api::PedestrianProfileEnum::WHEELCHAIR) ||\n          (query.requireBikeTransport_ &&\n           !stop.bikes_allowed(n::event_type::kArr)) ||\n          (query.requireCarTransport_ &&\n           !stop.cars_allowed(n::event_type::kArr))) {\n        continue;\n      }\n      for (auto const [offset, times] :\n           utl::zip(last_mile_taxi, last_mile_taxi_times)) {\n        if (nr::matches(tt, nr::location_match_mode::kExact, offset.target_,\n                        stop.get_location_idx()) &&\n            utl::any_of(times, [&](auto const& t) {\n              return t.contains(stop.time(n::event_type::kArr)) &&\n                     t.contains(stop.time(n::event_type::kArr) +\n                                offset.duration_ - 1min);\n            })) {\n          if (offset.duration_ < min_odm_duration) {\n            min_stop_idx = stop.stop_idx_;\n            min_odm_duration = offset.duration_;\n            shorter_ride = {.time_at_start_ = stop.time(n::event_type::kArr) +\n                                              offset.duration_,\n                            .time_at_stop_ = stop.time(n::event_type::kArr),\n                            .stop_ = offset.target_};\n          }\n          break;\n        }\n      }\n    }\n    if (shorter_ride) {\n      auto& odm_offset = std::get<nr::offset>(odm_leg.uses_);\n\n      auto const old_stop = odm_leg.from_;\n      auto const old_odm_time = std::chrono::minutes{odm_offset.duration_};\n      auto const old_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_;\n\n      ree.stop_range_.to_ = min_stop_idx + 1U;\n      pt_leg.to_ = odm_leg.from_ = odm_offset.target_ = shorter_ride->stop_;\n      pt_leg.arr_time_ = odm_leg.dep_time_ = shorter_ride->time_at_stop_;\n      odm_offset.duration_ = min_odm_duration;\n      j.dest_time_ = odm_leg.arr_time_ = shorter_ride->time_at_start_;\n\n      auto const new_stop = odm_leg.from_;\n      auto const new_odm_time = std::chrono::minutes{odm_offset.duration_};\n      auto const new_pt_time = pt_leg.arr_time_ - pt_leg.dep_time_;\n\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"shorten last leg: [stop: {}, ODM: {}, PT: {}] -> [stop: {}, \"\n             \"ODM: {}, PT: {}] (ODM: -{}, PT: +{})\",\n             n::loc{tt, old_stop}, old_odm_time, old_pt_time,\n             n::loc{tt, new_stop}, new_odm_time, new_pt_time,\n             std::chrono::minutes{old_odm_time - new_odm_time},\n             new_pt_time - old_pt_time);\n    }\n  };\n\n  for (auto& j : odm_journeys) {\n    if (j.legs_.empty()) {\n      n::log(n::log_lvl::debug, \"motis.prima\", \"shorten: journey without legs\");\n      continue;\n    }\n    shorten_first_leg(j);\n    shorten_last_leg(j);\n  }\n}\n\n}  // namespace motis::odm\n"
  },
  {
    "path": "src/odm/td_offsets.cc",
    "content": "#include \"motis/odm/td_offsets.h\"\n\n#include <ranges>\n\nusing namespace std::chrono_literals;\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\n\nnamespace motis::odm {\n\nstd::pair<nr::td_offsets_t, nr::td_offsets_t> get_td_offsets_split(\n    std::vector<nr::offset> const& offsets,\n    std::vector<service_times_t> const& times,\n    n::transport_mode_id_t const mode) {\n  auto const split =\n      offsets.empty()\n          ? 0\n          : std::distance(begin(offsets),\n                          std::upper_bound(begin(offsets), end(offsets),\n                                           offsets[offsets.size() / 2],\n                                           [](auto const& a, auto const& b) {\n                                             return a.duration_ < b.duration_;\n                                           }));\n\n  auto const offsets_lo = offsets | std::views::take(split);\n  auto const times_lo = times | std::views::take(split);\n  auto const offsets_hi = offsets | std::views::drop(split);\n  auto const times_hi = times | std::views::drop(split);\n\n  auto const derive_td_offsets = [&](auto const& offsets_split,\n                                     auto const& times_split) {\n    auto td_offsets = nr::td_offsets_t{};\n    for (auto const [o, t] : std::views::zip(offsets_split, times_split)) {\n      td_offsets.emplace(o.target_, std::vector<nr::td_offset>{});\n      for (auto const& i : t) {\n        td_offsets[o.target_].emplace_back(i.from_, o.duration_, mode);\n        td_offsets[o.target_].emplace_back(i.to_ - o.duration_ + 1min,\n                                           n::footpath::kMaxDuration, mode);\n      }\n    }\n    return td_offsets;\n  };\n\n  return std::pair{derive_td_offsets(offsets_lo, times_lo),\n                   derive_td_offsets(offsets_hi, times_hi)};\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/whitelist_ridesharing.cc",
    "content": "#include \"motis/odm/prima.h\"\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/json.hpp\"\n\n#include \"motis/http_req.h\"\n#include \"motis/odm/odm.h\"\n\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\nnamespace json = boost::json;\nusing namespace std::chrono_literals;\n\nnamespace motis::odm {\n\nstd::string prima::make_ride_sharing_request(n::timetable const& tt) const {\n  return make_whitelist_request(from_, to_, first_mile_ride_sharing_,\n                                last_mile_ride_sharing_, direct_ride_sharing_,\n                                fixed_, cap_, tt);\n}\n\nbool prima::consume_ride_sharing_response(std::string_view json) {\n  auto const update_first_mile = [&](json::array const& update) {\n    auto const n = n_rides_in_response(update);\n    if (first_mile_ride_sharing_.size() != n) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[whitelist taxi] first mile ride-sharing #rides != #updates ({} \"\n             \"!= {})\",\n             first_mile_ride_sharing_.size(), n);\n      first_mile_ride_sharing_.clear();\n      return true;\n    }\n\n    auto prev_first_mile =\n        std::exchange(first_mile_ride_sharing_, std::vector<nr::start>{});\n    auto prev_it = std::begin(prev_first_mile);\n    for (auto const& stop : update) {\n      for (auto const& time : stop.as_array()) {\n        if (!time.is_null() && time.is_array()) {\n          for (auto const& event : time.as_array()) {\n            first_mile_ride_sharing_.push_back(\n                {.time_at_start_ =\n                     to_unix(event.as_object().at(\"pickupTime\").as_int64()),\n                 .time_at_stop_ =\n                     to_unix(event.as_object().at(\"dropoffTime\").as_int64()) +\n                     kODMTransferBuffer,\n                 .stop_ = prev_it->stop_});\n            first_mile_ride_sharing_tour_ids_.emplace_back(\n                event.as_object().at(\"tripId\").as_string());\n          }\n        }\n        ++prev_it;\n      }\n    }\n    return false;\n  };\n\n  auto const update_last_mile = [&](json::array const& update) {\n    auto const n = n_rides_in_response(update);\n    if (last_mile_ride_sharing_.size() != n) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[whitelist taxi] last mile ride-sharing #rides != #updates ({} \"\n             \"!= {})\",\n             last_mile_ride_sharing_.size(), n);\n      last_mile_ride_sharing_.clear();\n      return true;\n    }\n\n    auto prev_last_mile =\n        std::exchange(last_mile_ride_sharing_, std::vector<nr::start>{});\n    auto prev_it = std::begin(prev_last_mile);\n    for (auto const& stop : update) {\n      for (auto const& time : stop.as_array()) {\n        if (!time.is_null() && time.is_array()) {\n          for (auto const& event : time.as_array()) {\n            last_mile_ride_sharing_.push_back(\n                {.time_at_start_ =\n                     to_unix(event.as_object().at(\"dropoffTime\").as_int64()),\n                 .time_at_stop_ =\n                     to_unix(event.as_object().at(\"pickupTime\").as_int64()) -\n                     kODMTransferBuffer,\n                 .stop_ = prev_it->stop_});\n            last_mile_ride_sharing_tour_ids_.emplace_back(\n                event.as_object().at(\"tripId\").as_string());\n          }\n          ++prev_it;\n        }\n      }\n    }\n    return false;\n  };\n\n  auto const update_direct = [&](json::array const& update) {\n    if (direct_ride_sharing_.size() != update.size()) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[whitelist ride-sharing] direct ride-sharing #rides != \"\n             \"#updates \"\n             \"({} != {})\",\n             direct_ride_sharing_.size(), update.size());\n      direct_ride_sharing_.clear();\n      return true;\n    }\n\n    direct_ride_sharing_.clear();\n    for (auto const& time : update) {\n      if (time.is_array()) {\n        for (auto const& ride : time.as_array()) {\n          if (!ride.is_null()) {\n            direct_ride_sharing_.push_back(\n                {to_unix(ride.as_object().at(\"pickupTime\").as_int64()),\n                 to_unix(ride.as_object().at(\"dropoffTime\").as_int64())});\n            direct_ride_sharing_tour_ids_.emplace_back(\n                ride.as_object().at(\"tripId\").as_string());\n          }\n        }\n      }\n    }\n\n    return false;\n  };\n\n  auto with_errors = false;\n  try {\n    auto const o = json::parse(json).as_object();\n    with_errors |= update_first_mile(o.at(\"start\").as_array());\n    with_errors |= update_last_mile(o.at(\"target\").as_array());\n    with_errors |= update_direct(o.at(\"direct\").as_array());\n  } catch (std::exception const&) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist ride-sharing] could not parse response: {}\", json);\n    return false;\n  }\n  if (with_errors) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist ride-sharing] parsed response with errors: {}\", json);\n    return false;\n  }\n\n  return true;\n}\n\nbool prima::whitelist_ride_sharing(n::timetable const& tt) {\n  auto response = std::optional<std::string>{};\n  auto ioc = boost::asio::io_context{};\n  try {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist ride-sharing] request for {} events\",\n           n_ride_sharing_events());\n    boost::asio::co_spawn(\n        ioc,\n        [&]() -> boost::asio::awaitable<void> {\n          auto const prima_msg =\n              co_await http_POST(ride_sharing_whitelist_, kReqHeaders,\n                                 make_ride_sharing_request(tt), 30s);\n          response = get_http_body(prima_msg);\n        },\n        boost::asio::detached);\n    ioc.run();\n  } catch (std::exception const& e) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist ride-sharing] networking failed: {}\", e.what());\n    response = std::nullopt;\n  }\n  if (!response) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist ride share] failed, discarding ride share journeys\");\n    return false;\n  }\n\n  return consume_ride_sharing_response(*response);\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/odm/whitelist_taxi.cc",
    "content": "#include \"motis/odm/prima.h\"\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/asio/io_context.hpp\"\n#include \"boost/json.hpp\"\n\n#include \"utl/erase_duplicates.h\"\n\n#include \"motis/http_req.h\"\n#include \"motis/odm/odm.h\"\n#include \"motis/transport_mode_ids.h\"\n\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\nnamespace json = boost::json;\nusing namespace std::chrono_literals;\n\nnamespace motis::odm {\n\nstd::string prima::make_whitelist_taxi_request(\n    std::vector<nr::start> const& first_mile,\n    std::vector<nr::start> const& last_mile,\n    n::timetable const& tt) const {\n  return make_whitelist_request(from_, to_, first_mile, last_mile, direct_taxi_,\n                                fixed_, cap_, tt);\n}\n\nvoid extract_taxis(std::vector<nr::journey> const& journeys,\n                   std::vector<nr::start>& first_mile_taxi_rides,\n                   std::vector<nr::start>& last_mile_taxi_rides) {\n  for (auto const& j : journeys) {\n    if (!j.legs_.empty()) {\n      if (is_odm_leg(j.legs_.front(), kOdmTransportModeId)) {\n        first_mile_taxi_rides.push_back({\n            .time_at_start_ = j.legs_.front().dep_time_,\n            .time_at_stop_ = j.legs_.front().arr_time_,\n            .stop_ = j.legs_.front().to_,\n        });\n      }\n    }\n\n    if (j.legs_.size() > 1) {\n      if (is_odm_leg(j.legs_.back(), kOdmTransportModeId)) {\n        last_mile_taxi_rides.push_back({\n            .time_at_start_ = j.legs_.back().arr_time_,\n            .time_at_stop_ = j.legs_.back().dep_time_,\n            .stop_ = j.legs_.back().from_,\n        });\n      }\n    }\n  }\n\n  utl::erase_duplicates(first_mile_taxi_rides, by_stop, std::equal_to<>{});\n  utl::erase_duplicates(last_mile_taxi_rides, by_stop, std::equal_to<>{});\n}\n\nvoid prima::extract_taxis_for_persisting(\n    std::vector<nr::journey> const& journeys) {\n  whitelist_first_mile_locations_.clear();\n  whitelist_last_mile_locations_.clear();\n\n  for (auto const& j : journeys) {\n    if (j.legs_.size() <= 1) {\n      continue;\n    }\n\n    if (is_odm_leg(j.legs_.front(), kOdmTransportModeId)) {\n      whitelist_first_mile_locations_.push_back(j.legs_.front().to_);\n    }\n\n    if (is_odm_leg(j.legs_.back(), kOdmTransportModeId)) {\n      whitelist_last_mile_locations_.push_back(j.legs_.back().from_);\n    }\n  }\n\n  utl::erase_duplicates(whitelist_first_mile_locations_, std::less<>{},\n                        std::equal_to<>{});\n  utl::erase_duplicates(whitelist_last_mile_locations_, std::less<>{},\n                        std::equal_to<>{});\n}\n\nbool prima::consume_whitelist_taxi_response(\n    std::string_view json,\n    std::vector<nr::journey>& journeys,\n    std::vector<nr::start>& first_mile_taxi_rides,\n    std::vector<nr::start>& last_mile_taxi_rides) {\n  auto const update_first_mile = [&](json::array const& update) {\n    auto const n_pt_udpates = n_rides_in_response(update);\n    if (first_mile_taxi_rides.size() != n_pt_udpates) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[whitelist taxi] first mile taxi #rides != #updates ({} != {})\",\n             first_mile_taxi_rides.size(), n_pt_udpates);\n      return true;\n    }\n\n    auto const prev_first_mile =\n        std::exchange(first_mile_taxi_rides, std::vector<nr::start>{});\n\n    auto prev_it = std::begin(prev_first_mile);\n    for (auto const& stop : update) {\n      for (auto const& event : stop.as_array()) {\n        if (event.is_null()) {\n          first_mile_taxi_rides.push_back({.time_at_start_ = kInfeasible,\n                                           .time_at_stop_ = kInfeasible,\n                                           .stop_ = prev_it->stop_});\n        } else {\n          first_mile_taxi_rides.push_back({\n              .time_at_start_ =\n                  to_unix(event.as_object().at(\"pickupTime\").as_int64()),\n              .time_at_stop_ =\n                  to_unix(event.as_object().at(\"dropoffTime\").as_int64()),\n              .stop_ = prev_it->stop_,\n          });\n        }\n        ++prev_it;\n      }\n    }\n    fix_first_mile_duration(journeys, first_mile_taxi_rides, prev_first_mile,\n                            kOdmTransportModeId);\n    return false;\n  };\n\n  auto const update_last_mile = [&](json::array const& update) {\n    auto const n_pt_udpates = n_rides_in_response(update);\n    if (last_mile_taxi_rides.size() != n_pt_udpates) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[whitelist taxi] last mile taxi #rides != #updates ({} != {})\",\n             last_mile_taxi_rides.size(), n_pt_udpates);\n      return true;\n    }\n\n    auto const prev_last_mile =\n        std::exchange(last_mile_taxi_rides, std::vector<nr::start>{});\n\n    auto prev_it = std::begin(prev_last_mile);\n    for (auto const& stop : update) {\n      for (auto const& event : stop.as_array()) {\n        if (event.is_null()) {\n          last_mile_taxi_rides.push_back({\n              .time_at_start_ = kInfeasible,\n              .time_at_stop_ = kInfeasible,\n              .stop_ = prev_it->stop_,\n          });\n        } else {\n          last_mile_taxi_rides.push_back({\n              .time_at_start_ =\n                  to_unix(event.as_object().at(\"dropoffTime\").as_int64()),\n              .time_at_stop_ =\n                  to_unix(event.as_object().at(\"pickupTime\").as_int64()),\n              .stop_ = prev_it->stop_,\n          });\n        }\n        ++prev_it;\n      }\n    }\n\n    fix_last_mile_duration(journeys, last_mile_taxi_rides, prev_last_mile,\n                           kOdmTransportModeId);\n    return false;\n  };\n\n  auto const update_direct_rides = [&](json::array const& update) {\n    if (direct_taxi_.size() != update.size()) {\n      n::log(n::log_lvl::debug, \"motis.prima\",\n             \"[whitelist taxi] direct taxi #rides != #updates ({} != {})\",\n             direct_taxi_.size(), update.size());\n      direct_taxi_.clear();\n      return true;\n    }\n\n    direct_taxi_.clear();\n    for (auto const& ride : update) {\n      if (!ride.is_null()) {\n        direct_taxi_.push_back(\n            {to_unix(ride.as_object().at(\"pickupTime\").as_int64()),\n             to_unix(ride.as_object().at(\"dropoffTime\").as_int64())});\n      }\n    }\n\n    return false;\n  };\n\n  auto with_errors = false;\n  try {\n    auto const o = json::parse(json).as_object();\n    with_errors |= update_first_mile(o.at(\"start\").as_array());\n    with_errors |= update_last_mile(o.at(\"target\").as_array());\n    with_errors |= update_direct_rides(o.at(\"direct\").as_array());\n  } catch (std::exception const&) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist taxi] could not parse response: {}\", json);\n    return false;\n  }\n  if (with_errors) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist taxi] parsed response with errors: {}\", json);\n    return false;\n  }\n\n  // adjust journey start/dest times after adjusting legs\n  for (auto& j : journeys) {\n    if (!j.legs_.empty()) {\n      j.start_time_ = j.legs_.front().dep_time_;\n      j.dest_time_ = j.legs_.back().arr_time_;\n    }\n  }\n\n  return true;\n}\n\nbool prima::whitelist_taxi(std::vector<nr::journey>& taxi_journeys,\n                           n::timetable const& tt) {\n  auto first_mile_taxi_rides = std::vector<nr::start>{};\n  auto last_mile_taxi_rides = std::vector<nr::start>{};\n  extract_taxis(taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides);\n  extract_taxis_for_persisting(taxi_journeys);\n\n  auto whitelist_response = std::optional<std::string>{};\n  auto ioc = boost::asio::io_context{};\n  try {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist taxi] request for {} rides\",\n           first_mile_taxi_rides.size() + last_mile_taxi_rides.size() +\n               direct_taxi_.size());\n\n    boost::asio::co_spawn(\n        ioc,\n        [&]() -> boost::asio::awaitable<void> {\n          auto const prima_msg = co_await http_POST(\n              taxi_whitelist_, kReqHeaders,\n              make_whitelist_request(from_, to_, first_mile_taxi_rides,\n                                     last_mile_taxi_rides, direct_taxi_, fixed_,\n                                     cap_, tt),\n              10s);\n          whitelist_response = get_http_body(prima_msg);\n        },\n        boost::asio::detached);\n    ioc.run();\n  } catch (std::exception const& e) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist taxi] networking failed: {}\", e.what());\n    whitelist_response = std::nullopt;\n  }\n  if (!whitelist_response) {\n    n::log(n::log_lvl::debug, \"motis.prima\",\n           \"[whitelist taxi] failed, discarding taxi journeys\");\n    return false;\n  }\n  auto const was_whitelist_response_valid = consume_whitelist_taxi_response(\n      *whitelist_response, taxi_journeys, first_mile_taxi_rides,\n      last_mile_taxi_rides);\n  if (!was_whitelist_response_valid) {\n    return false;\n  }\n  whitelist_response_ = json::parse(whitelist_response.value()).as_object();\n  return true;\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "src/osr/max_distance.cc",
    "content": "#include \"motis/osr/max_distance.h\"\n\n#include <utility>\n\nnamespace motis {\n\ndouble get_max_distance(osr::search_profile const profile,\n                        std::chrono::seconds const t) {\n  auto seconds = static_cast<double>(t.count());\n  switch (profile) {\n    case osr::search_profile::kWheelchair: return seconds * 0.8;\n    case osr::search_profile::kFoot: return seconds * 1.1;\n    case osr::search_profile::kBikeSharing: [[fallthrough]];\n    case osr::search_profile::kBikeElevationLow: [[fallthrough]];\n    case osr::search_profile::kBikeElevationHigh: [[fallthrough]];\n    case osr::search_profile::kBikeFast: [[fallthrough]];\n    case osr::search_profile::kBike: return seconds * 4.0;\n    case osr::search_profile::kCar: [[fallthrough]];\n    case osr::search_profile::kCarDropOff: [[fallthrough]];\n    case osr::search_profile::kCarDropOffWheelchair: [[fallthrough]];\n    case osr::search_profile::kCarSharing: [[fallthrough]];\n    case osr::search_profile::kCarParking: [[fallthrough]];\n    case osr::search_profile::kCarParkingWheelchair: return seconds * 28.0;\n    case osr::search_profile::kBus: return seconds * 5.0;\n    case osr::search_profile::kRailway: return seconds * 5.5;\n    case osr::search_profile::kFerry: return seconds * 4.0;\n  }\n  std::unreachable();\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/osr/mode_to_profile.cc",
    "content": "#include \"motis/osr/mode_to_profile.h\"\n\n#include \"utl/verify.h\"\n\nnamespace motis {\n\napi::ModeEnum to_mode(osr::mode const m) {\n  switch (m) {\n    case osr::mode::kFoot: [[fallthrough]];\n    case osr::mode::kWheelchair: return api::ModeEnum::WALK;\n    case osr::mode::kBike: return api::ModeEnum::BIKE;\n    case osr::mode::kCar: return api::ModeEnum::CAR;\n    case osr::mode::kRailway: return api::ModeEnum::DEBUG_RAILWAY_ROUTE;\n    case osr::mode::kFerry: return api::ModeEnum::DEBUG_FERRY_ROUTE;\n  }\n  std::unreachable();\n}\n\nosr::search_profile to_profile(\n    api::ModeEnum const m,\n    api::PedestrianProfileEnum const pedestrian_profile,\n    api::ElevationCostsEnum const elevation_costs) {\n  auto const wheelchair =\n      pedestrian_profile == api::PedestrianProfileEnum::WHEELCHAIR;\n  switch (m) {\n    case api::ModeEnum::WALK:\n      return wheelchair ? osr::search_profile::kWheelchair\n                        : osr::search_profile::kFoot;\n    case api::ModeEnum::BIKE:\n      switch (elevation_costs) {\n        case api::ElevationCostsEnum::NONE: return osr::search_profile::kBike;\n        case api::ElevationCostsEnum::LOW:\n          return osr::search_profile::kBikeElevationLow;\n        case api::ElevationCostsEnum::HIGH:\n          return osr::search_profile::kBikeElevationHigh;\n      }\n      return osr::search_profile::kBike;  // Fallback if invalid value is used\n    case api::ModeEnum::ODM: [[fallthrough]];\n    case api::ModeEnum::RIDE_SHARING: [[fallthrough]];\n    case api::ModeEnum::CAR: return osr::search_profile::kCar;\n    case api::ModeEnum::CAR_DROPOFF:\n      return wheelchair ? osr::search_profile::kCarDropOffWheelchair\n                        : osr::search_profile::kCarDropOff;\n    case api::ModeEnum::CAR_PARKING:\n      return wheelchair ? osr::search_profile::kCarParkingWheelchair\n                        : osr::search_profile::kCarParking;\n    case api::ModeEnum::RENTAL:\n      // could be kBikeSharing or kCarSharing, use gbfs::get_osr_profile()\n      // to get the correct profile for each product\n      return osr::search_profile::kBikeSharing;\n    case api::ModeEnum::DEBUG_BUS_ROUTE: return osr::search_profile::kBus;\n    case api::ModeEnum::DEBUG_RAILWAY_ROUTE:\n      return osr::search_profile::kRailway;\n    case api::ModeEnum::DEBUG_FERRY_ROUTE: return osr::search_profile::kFerry;\n    default: throw utl::fail(\"unsupported mode\");\n  }\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/osr/parameters.cc",
    "content": "#include \"motis/osr/parameters.h\"\n\n#include <optional>\n#include <type_traits>\n\n#include \"utl/verify.h\"\n\n#include \"osr/routing/profiles/bike.h\"\n#include \"osr/routing/profiles/bike_sharing.h\"\n#include \"osr/routing/profiles/car.h\"\n#include \"osr/routing/profiles/car_parking.h\"\n#include \"osr/routing/profiles/car_sharing.h\"\n#include \"osr/routing/profiles/foot.h\"\n\nnamespace motis {\n\ntemplate <typename T>\nconcept HasPedestrianProfile =\n    requires(T const& params) { params.pedestrianProfile_; };\n\ntemplate <typename T>\nconcept HasPedestrianProfileAndSpeed =\n    HasPedestrianProfile<T> &&\n    std::is_same_v<decltype(std::declval<T>().pedestrianSpeed_),\n                   std::optional<double>>;\n\ntemplate <typename T>\nbool use_wheelchair(T const&) {\n  return false;\n}\n\ntemplate <typename T>\nbool use_wheelchair(T const& t)\n  requires HasPedestrianProfile<T>\n{\n  return t.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR;\n}\n\ntemplate <typename T>\nfloat pedestrian_speed(T const&) {\n  return osr_parameters::kFootSpeed;\n}\n\ntemplate <>\nfloat pedestrian_speed(api::PedestrianProfileEnum const& p) {\n  return p == api::PedestrianProfileEnum::FOOT\n             ? osr_parameters::kFootSpeed\n             : osr_parameters::kWheelchairSpeed;\n}\n\ntemplate <typename T>\nfloat pedestrian_speed(T const& params)\n  requires HasPedestrianProfile<T>\n{\n  return pedestrian_speed(params.pedestrianProfile_);\n}\n\ntemplate <typename T>\nfloat pedestrian_speed(T const& params)\n  requires HasPedestrianProfileAndSpeed<T>\n{\n  return params.pedestrianSpeed_\n      .and_then([](auto const speed) {\n        return speed > 0.3 && speed < 10.0\n                   ? std::optional{static_cast<float>(speed)}\n                   : std::nullopt;\n      })\n      .value_or(pedestrian_speed(params.pedestrianProfile_));\n}\n\ntemplate <typename T>\nfloat cycling_speed(T const&) {\n  return osr_parameters::kBikeSpeed;\n}\n\ntemplate <typename T>\nfloat cycling_speed(T const& params)\n  requires(\n      std::is_same_v<decltype(params.cyclingSpeed_), std::optional<double>>)\n{\n  return params.cyclingSpeed_\n      .and_then([](auto const speed) {\n        return speed > 0.7 && speed < 20.0\n                   ? std::optional{static_cast<float>(speed)}\n                   : std::nullopt;\n      })\n      .value_or(osr_parameters::kBikeSpeed);\n}\n\ntemplate <typename T>\nosr_parameters to_osr_parameters(T const& params) {\n  return {\n      .pedestrian_speed_ = pedestrian_speed(params),\n      .cycling_speed_ = cycling_speed(params),\n      .use_wheelchair_ = use_wheelchair(params),\n  };\n}\n\nosr_parameters get_osr_parameters(api::plan_params const& params) {\n  return to_osr_parameters(params);\n}\n\nosr_parameters get_osr_parameters(api::oneToAll_params const& params) {\n  return to_osr_parameters(params);\n}\n\nosr_parameters get_osr_parameters(api::oneToMany_params const& params) {\n  return to_osr_parameters(params);\n}\n\nosr_parameters get_osr_parameters(api::OneToManyParams const& params) {\n  return to_osr_parameters(params);\n}\n\nosr_parameters get_osr_parameters(\n    api::oneToManyIntermodal_params const& params) {\n  return to_osr_parameters(params);\n}\n\nosr_parameters get_osr_parameters(\n    api::OneToManyIntermodalParams const& params) {\n  return to_osr_parameters(params);\n}\n\nosr::profile_parameters to_profile_parameters(osr::search_profile const p,\n                                              osr_parameters const& params) {\n  // Ensure correct speed is used when using default parameters\n  auto const wheelchair_speed = params.use_wheelchair_\n                                    ? params.pedestrian_speed_\n                                    : osr_parameters::kWheelchairSpeed;\n  switch (p) {\n    case osr::search_profile::kFoot:\n      return osr::foot<false, osr::elevator_tracking>::parameters{\n          .speed_meters_per_second_ = params.pedestrian_speed_};\n    case osr::search_profile::kWheelchair:\n      return osr::foot<true, osr::elevator_tracking>::parameters{\n          .speed_meters_per_second_ = wheelchair_speed};\n    case osr::search_profile::kBike:\n      return osr::bike<osr::bike_costing::kSafe,\n                       osr::kElevationNoCost>::parameters{\n          .speed_meters_per_second_ = params.cycling_speed_};\n    case osr::search_profile::kBikeFast:\n      return osr::bike<osr::bike_costing::kFast,\n                       osr::kElevationNoCost>::parameters{\n          .speed_meters_per_second_ = params.cycling_speed_};\n    case osr::search_profile::kBikeElevationLow:\n      return osr::bike<osr::bike_costing::kSafe,\n                       osr::kElevationLowCost>::parameters{\n          .speed_meters_per_second_ = params.cycling_speed_};\n    case osr::search_profile::kBikeElevationHigh:\n      return osr::bike<osr::bike_costing::kSafe,\n                       osr::kElevationHighCost>::parameters{\n          .speed_meters_per_second_ = params.cycling_speed_};\n    case osr::search_profile::kCar: return osr::car::parameters{};\n    case osr::search_profile::kCarDropOff:\n      return osr::car_parking<false, false>::parameters{\n          .car_ = {},\n          .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}};\n    case osr::search_profile::kCarDropOffWheelchair:\n      return osr::car_parking<true, false>::parameters{\n          .car_ = {}, .foot_ = {.speed_meters_per_second_ = wheelchair_speed}};\n    case osr::search_profile::kCarParking:\n      return osr::car_parking<false, true>::parameters{\n          .car_ = {},\n          .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}};\n    case osr::search_profile::kCarParkingWheelchair:\n      return osr::car_parking<true, true>::parameters{\n          .car_ = {}, .foot_ = {.speed_meters_per_second_ = wheelchair_speed}};\n    case osr::search_profile::kBikeSharing:\n      return osr::bike_sharing::parameters{\n          .bike_ = {.speed_meters_per_second_ = params.cycling_speed_},\n          .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}};\n    case osr::search_profile::kCarSharing:\n      return osr::car_sharing<osr::track_node_tracking>::parameters{\n          .car_ = {},\n          .foot_ = {.speed_meters_per_second_ = params.pedestrian_speed_}};\n    case osr::search_profile::kBus: return osr::bus::parameters{};\n    case osr::search_profile::kRailway: return osr::railway::parameters{};\n    case osr::search_profile::kFerry: return osr::ferry::parameters{};\n  }\n  throw utl::fail(\"{} is not a valid profile\", static_cast<std::uint8_t>(p));\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/osr/street_routing.cc",
    "content": "#include \"motis/osr/street_routing.h\"\n\n#include \"geo/polyline_format.h\"\n\n#include \"utl/concat.h\"\n#include \"utl/get_or_create.h\"\n\n#include \"osr/routing/algorithms.h\"\n#include \"osr/routing/parameters.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/routing/sharing_data.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/osr/mode_to_profile.h\"\n#include \"motis/place.h\"\n#include \"motis/polyline.h\"\n#include \"motis/transport_mode_ids.h\"\n#include \"motis/update_rtt_td_footpaths.h\"\n#include \"utl/verify.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\ndefault_output::default_output(osr::ways const& w,\n                               osr::search_profile const profile)\n    : w_{w},\n      profile_{profile},\n      id_{static_cast<std::underlying_type_t<osr::search_profile>>(profile)} {}\n\ndefault_output::default_output(osr::ways const& w,\n                               nigiri::transport_mode_id_t const id)\n    : w_{w},\n      profile_{id == kOdmTransportModeId || id == kRideSharingTransportModeId\n                   ? osr::search_profile::kCar\n                   : osr::search_profile{static_cast<\n                         std::underlying_type_t<osr::search_profile>>(id)}},\n      id_{id} {\n  utl::verify(id <= kRideSharingTransportModeId, \"invalid mode id={}\", id);\n}\n\ndefault_output::~default_output() = default;\n\napi::ModeEnum default_output::get_mode() const {\n  if (id_ == kOdmTransportModeId) {\n    return api::ModeEnum::ODM;\n  }\n  if (id_ == kRideSharingTransportModeId) {\n    return api::ModeEnum::RIDE_SHARING;\n  }\n\n  switch (profile_) {\n    case osr::search_profile::kFoot: [[fallthrough]];\n    case osr::search_profile::kWheelchair: return api::ModeEnum::WALK;\n    case osr::search_profile::kBike: [[fallthrough]];\n    case osr::search_profile::kBikeFast: [[fallthrough]];\n    case osr::search_profile::kBikeElevationLow: [[fallthrough]];\n    case osr::search_profile::kBikeElevationHigh: return api::ModeEnum::BIKE;\n    case osr::search_profile::kCar: return api::ModeEnum::CAR;\n    case osr::search_profile::kCarParking: [[fallthrough]];\n    case osr::search_profile::kCarParkingWheelchair:\n      return api::ModeEnum::CAR_PARKING;\n    case osr::search_profile::kCarDropOff: [[fallthrough]];\n    case osr::search_profile::kCarDropOffWheelchair:\n      return api::ModeEnum::CAR_DROPOFF;\n    case osr::search_profile::kBikeSharing: [[fallthrough]];\n    case osr::search_profile::kCarSharing: return api::ModeEnum::RENTAL;\n    case osr::search_profile::kBus: return api::ModeEnum::DEBUG_BUS_ROUTE;\n    case osr::search_profile::kRailway:\n      return api::ModeEnum::DEBUG_RAILWAY_ROUTE;\n    case osr::search_profile::kFerry: return api::ModeEnum::DEBUG_FERRY_ROUTE;\n  }\n\n  return api::ModeEnum::OTHER;\n}\n\nosr::search_profile default_output::get_profile() const { return profile_; }\n\napi::Place default_output::get_place(\n    nigiri::lang_t const&,\n    osr::node_idx_t const n,\n    std::optional<std::string> const& tz) const {\n  auto const pos = w_.get_node_pos(n).as_latlng();\n  return api::Place{.lat_ = pos.lat_,\n                    .lon_ = pos.lng_,\n                    .tz_ = tz,\n                    .vertexType_ = api::VertexTypeEnum::NORMAL};\n}\n\nbool default_output::is_time_dependent() const {\n  return profile_ == osr::search_profile::kWheelchair ||\n         profile_ == osr::search_profile::kCarParkingWheelchair ||\n         profile_ == osr::search_profile::kCarDropOffWheelchair;\n}\n\ntransport_mode_t default_output::get_cache_key() const {\n  return static_cast<transport_mode_t>(profile_);\n}\n\nosr::sharing_data const* default_output::get_sharing_data() const {\n  return nullptr;\n}\n\nvoid default_output::annotate_leg(n::lang_t const&,\n                                  osr::node_idx_t,\n                                  osr::node_idx_t,\n                                  api::Leg&) const {}\n\nstd::vector<api::StepInstruction> get_step_instructions(\n    osr::ways const& w,\n    osr::elevation_storage const* elevations,\n    osr::location const& from,\n    osr::location const& to,\n    std::span<osr::path::segment const> segments,\n    unsigned const api_version) {\n  auto steps = std::vector<api::StepInstruction>{};\n  auto pred_lvl = from.lvl_.to_float();\n  for (auto const& s : segments) {\n    if (s.from_ != osr::node_idx_t::invalid() && s.from_ < w.n_nodes() &&\n        w.r_->node_properties_[s.from_].is_elevator()) {\n      steps.push_back(api::StepInstruction{\n          .relativeDirection_ = api::DirectionEnum::ELEVATOR,\n          .fromLevel_ = pred_lvl,\n          .toLevel_ = s.from_level_.to_float()});\n    }\n\n    auto const way_name = s.way_ == osr::way_idx_t::invalid()\n                              ? osr::string_idx_t::invalid()\n                              : w.way_names_[s.way_];\n    auto const props = s.way_ != osr::way_idx_t::invalid()\n                           ? w.r_->way_properties_[s.way_]\n                           : osr::way_properties{};\n    steps.push_back(api::StepInstruction{\n        .relativeDirection_ =\n            s.way_ != osr::way_idx_t::invalid()\n                ? (props.is_elevator() ? api::DirectionEnum::ELEVATOR\n                   : props.is_steps()  ? api::DirectionEnum::STAIRS\n                                       : api::DirectionEnum::CONTINUE)\n                : api::DirectionEnum::CONTINUE,  // TODO entry/exit/u-turn\n        .distance_ = static_cast<double>(s.dist_),\n        .fromLevel_ = s.from_level_.to_float(),\n        .toLevel_ = s.to_level_.to_float(),\n        .osmWay_ = s.way_ == osr::way_idx_t ::invalid()\n                       ? std::nullopt\n                       : std::optional{static_cast<std::int64_t>(\n                             to_idx(w.way_osm_idx_[s.way_]))},\n        .polyline_ = api_version == 1 ? to_polyline<7>(s.polyline_)\n                                      : to_polyline<6>(s.polyline_),\n        .streetName_ = way_name == osr::string_idx_t::invalid()\n                           ? \"\"\n                           : std::string{w.strings_[way_name].view()},\n        .exit_ = {},  // TODO\n        .stayOn_ = false,  // TODO\n        .area_ = false,  // TODO\n        .toll_ = props.has_toll(),\n        .accessRestriction_ = w.get_access_restriction(s.way_).and_then(\n            [](std::string_view s) { return std::optional{std::string{s}}; }),\n        .elevationUp_ =\n            elevations ? std::optional{to_idx(s.elevation_.up_)} : std::nullopt,\n        .elevationDown_ = elevations ? std::optional{to_idx(s.elevation_.down_)}\n                                     : std::nullopt});\n  }\n\n  if (!segments.empty()) {\n    auto& last = segments.back();\n    if (last.to_ != osr::node_idx_t::invalid() && last.to_ < w.n_nodes() &&\n        w.r_->node_properties_[last.to_].is_elevator()) {\n      steps.push_back(api::StepInstruction{\n          .relativeDirection_ = api::DirectionEnum::ELEVATOR,\n          .fromLevel_ = pred_lvl,\n          .toLevel_ = to.lvl_.to_float()});\n    }\n  }\n\n  return steps;\n}\n\napi::Itinerary dummy_itinerary(api::Place const& from,\n                               api::Place const& to,\n                               api::ModeEnum const mode,\n                               n::unixtime_t const start_time,\n                               n::unixtime_t const end_time) {\n  auto itinerary = api::Itinerary{\n      .duration_ = std::chrono::duration_cast<std::chrono::seconds>(end_time -\n                                                                    start_time)\n                       .count(),\n      .startTime_ = start_time,\n      .endTime_ = end_time};\n  auto& leg = itinerary.legs_.emplace_back(api::Leg{\n      .mode_ = mode,\n      .from_ = from,\n      .to_ = to,\n      .duration_ = std::chrono::duration_cast<std::chrono::seconds>(end_time -\n                                                                    start_time)\n                       .count(),\n      .startTime_ = start_time,\n      .endTime_ = end_time,\n      .scheduledStartTime_ = start_time,\n      .scheduledEndTime_ = end_time,\n      .legGeometry_ = empty_polyline()});\n  leg.from_.pickupType_ = std::nullopt;\n  leg.from_.dropoffType_ = std::nullopt;\n  leg.to_.pickupType_ = std::nullopt;\n  leg.to_.dropoffType_ = std::nullopt;\n  leg.from_.departure_ = leg.from_.scheduledDeparture_ = leg.startTime_;\n  leg.to_.arrival_ = leg.to_.scheduledArrival_ = leg.endTime_;\n  return itinerary;\n}\n\napi::Itinerary street_routing(osr::ways const& w,\n                              osr::lookup const& l,\n                              elevators const* e,\n                              osr::elevation_storage const* elevations,\n                              n::lang_t const& lang,\n                              api::Place const& from_place,\n                              api::Place const& to_place,\n                              output const& out,\n                              std::optional<n::unixtime_t> const start_time,\n                              std::optional<n::unixtime_t> const end_time,\n                              double const max_matching_distance,\n                              osr_parameters const& osr_params,\n                              street_routing_cache_t& cache,\n                              osr::bitvec<osr::node_idx_t>& blocked_mem,\n                              unsigned const api_version,\n                              bool const detailed_leg,\n                              std::chrono::seconds const max) {\n  utl::verify(start_time.has_value() || end_time.has_value(),\n              \"either start_time or end_time must be set\");\n  auto const bound_time =\n      start_time.or_else([&]() { return end_time; }).value();\n  auto const from = get_location(from_place);\n  auto const to = get_location(to_place);\n  auto const s = e ? get_states_at(w, l, *e, bound_time, from.pos_)\n                   : std::optional{std::pair<nodes_t, states_t>{}};\n  auto const cache_key = street_routing_cache_key_t{\n      from, to, out.get_cache_key(),\n      out.is_time_dependent() ? bound_time : n::unixtime_t{n::i32_minutes{0}}};\n  auto const path = utl::get_or_create(cache, cache_key, [&]() {\n    auto const& [e_nodes, e_states] = *s;\n    auto const profile = out.get_profile();\n    return osr::route(\n        to_profile_parameters(profile, osr_params), w, l, profile, from, to,\n        static_cast<osr::cost_t>(max.count()), osr::direction::kForward,\n        max_matching_distance,\n        s ? &set_blocked(e_nodes, e_states, blocked_mem) : nullptr,\n        out.get_sharing_data(), elevations, osr::routing_algorithm::kAStarBi);\n  });\n\n  if (!path.has_value()) {\n    if (!start_time.has_value() || !end_time.has_value()) {\n      return {};\n    }\n    return dummy_itinerary(from_place, to_place, out.get_mode(), *start_time,\n                           *end_time);\n  }\n\n  auto const deduced_start_time =\n      start_time ? *start_time : *end_time - std::chrono::seconds{path->cost_};\n  auto itinerary = api::Itinerary{\n      .duration_ = start_time && end_time\n                       ? std::chrono::duration_cast<std::chrono::seconds>(\n                             *end_time - *start_time)\n                             .count()\n                       : path->cost_,\n      .startTime_ = deduced_start_time,\n      .endTime_ = end_time ? *end_time\n                           : *start_time + std::chrono::seconds{path->cost_},\n      .transfers_ = 0};\n\n  auto t =\n      std::chrono::time_point_cast<std::chrono::seconds>(deduced_start_time);\n  auto pred_place = from_place;\n  auto pred_end_time = t;\n  utl::equal_ranges_linear(\n      path->segments_,\n      [](osr::path::segment const& a, osr::path::segment const& b) {\n        return a.mode_ == b.mode_;\n      },\n      [&](std::vector<osr::path::segment>::const_iterator const& lb,\n          std::vector<osr::path::segment>::const_iterator const& ub) {\n        auto const range = std::span{lb, ub};\n        auto const is_last_leg = ub == end(path->segments_);\n        auto const from_node = range.front().from_;\n        auto const to_node = range.back().to_;\n\n        auto concat = geo::polyline{};\n        auto dist = 0.0;\n        for (auto const& p : range) {\n          utl::concat(concat, p.polyline_);\n          if (p.cost_ != osr::kInfeasible) {\n            t += std::chrono::seconds{p.cost_};\n            dist += p.dist_;\n          }\n        }\n\n        auto& leg = itinerary.legs_.emplace_back(api::Leg{\n            .mode_ = out.get_mode() == api::ModeEnum::ODM\n                         ? api::ModeEnum::ODM\n                         : (out.get_mode() == api::ModeEnum::RIDE_SHARING\n                                ? api::ModeEnum::RIDE_SHARING\n                                : to_mode(lb->mode_)),\n            .from_ = pred_place,\n            .to_ = is_last_leg ? to_place\n                               : out.get_place(lang, to_node, pred_place.tz_),\n            .duration_ = std::chrono::duration_cast<std::chrono::seconds>(\n                             t - pred_end_time)\n                             .count(),\n            .startTime_ = pred_end_time,\n            .endTime_ = is_last_leg && end_time ? *end_time : t,\n            .distance_ = dist,\n            .legGeometry_ = detailed_leg\n                                ? (api_version == 1 ? to_polyline<7>(concat)\n                                                    : to_polyline<6>(concat))\n                                : empty_polyline()});\n\n        leg.from_.departure_ = leg.from_.scheduledDeparture_ =\n            leg.scheduledStartTime_ = leg.startTime_;\n        leg.to_.arrival_ = leg.to_.scheduledArrival_ = leg.scheduledEndTime_ =\n            leg.endTime_;\n        leg.from_.pickupType_ = std::nullopt;\n        leg.from_.dropoffType_ = std::nullopt;\n        leg.to_.pickupType_ = std::nullopt;\n        leg.to_.dropoffType_ = std::nullopt;\n\n        if (detailed_leg) {\n          leg.steps_ = get_step_instructions(w, elevations, from, to, range,\n                                             api_version);\n        }\n\n        out.annotate_leg(lang, from_node, to_node, leg);\n\n        pred_place = leg.to_;\n        pred_end_time = t;\n      });\n\n  if (end_time && !itinerary.legs_.empty()) {\n    auto& last = itinerary.legs_.back();\n    last.to_.arrival_ = last.to_.scheduledArrival_ = last.endTime_ =\n        last.scheduledEndTime_ = *end_time;\n    for (auto& leg : itinerary.legs_) {\n      leg.duration_ = (leg.endTime_.time_ - leg.startTime_.time_).count();\n    }\n  }\n\n  return itinerary;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/parse_location.cc",
    "content": "#include \"motis/parse_location.h\"\n\n#include <utility>\n\n#include \"boost/phoenix/core/reference.hpp\"\n#include \"boost/spirit/include/qi.hpp\"\n\n#include \"date/date.h\"\n\n#include \"utl/parser/arg_parser.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nstd::optional<osr::location> parse_location(std::string_view s,\n                                            char const separator) {\n  using boost::phoenix::ref;\n  using boost::spirit::ascii::space;\n  using boost::spirit::qi::double_;\n  using boost::spirit::qi::phrase_parse;\n\n  auto first = begin(s);\n  auto last = end(s);\n\n  auto pos = geo::latlng{};\n  auto level = osr::kNoLevel;\n  auto const lat = [&](double& x) { pos.lat_ = x; };\n  auto const lng = [&](double& x) { pos.lng_ = x; };\n  auto const lvl = [&](double& x) {\n    level = osr::level_t{static_cast<float>(x)};\n  };\n\n  auto const has_matched =\n      phrase_parse(first, last,\n                   ((double_[lat] >> separator >> double_[lng] >> separator >>\n                     double_[lvl]) |\n                    double_[lat] >> separator >> double_[lng]),\n                   space);\n  if (!has_matched || first != last) {\n    return std::nullopt;\n  }\n\n  return osr::location{pos, level};\n}\n\ndate::sys_days parse_iso_date(std::string_view s) {\n  auto d = date::sys_days{};\n  (std::stringstream{} << s) >> date::parse(\"%F\", d);\n  return d;\n}\n\nstd::pair<n::direction, n::unixtime_t> parse_cursor(std::string_view s) {\n  auto const split_pos = s.find(\"|\");\n  utl::verify(split_pos != std::string_view::npos && split_pos != s.size() - 1U,\n              \"invalid page cursor {}, separator '|' not found\", s);\n\n  auto const time_str = s.substr(split_pos + 1U);\n  utl::verify(\n      utl::all_of(time_str, [&](auto&& c) { return std::isdigit(c) != 0U; }),\n      \"invalid page cursor \\\"{}\\\", timestamp not a number\", s);\n\n  auto const t = n::unixtime_t{std::chrono::duration_cast<n::i32_minutes>(\n      std::chrono::seconds{utl::parse<std::int64_t>(time_str)})};\n\n  auto const direction = s.substr(0, split_pos);\n  switch (cista::hash(direction)) {\n    case cista::hash(\"EARLIER\"): return {n::direction::kBackward, t};\n    case cista::hash(\"LATER\"): return {n::direction::kForward, t};\n    default: throw utl::fail(\"invalid cursor: \\\"{}\\\"\", s);\n  }\n}\n\nn::routing::query cursor_to_query(std::string_view s) {\n  auto const [dir, t] = parse_cursor(s);\n  switch (dir) {\n    case n::direction::kBackward:\n      return n::routing::query{\n          .start_time_ =\n              n::routing::start_time_t{n::interval{t - n::duration_t{120}, t}},\n          .extend_interval_earlier_ = true,\n          .extend_interval_later_ = false};\n\n    case n::direction::kForward:\n      return n::routing::query{\n          .start_time_ =\n              n::routing::start_time_t{n::interval{t, t + n::duration_t{120}}},\n          .extend_interval_earlier_ = false,\n          .extend_interval_later_ = true};\n  }\n  std::unreachable();\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/place.cc",
    "content": "#include \"motis/place.h\"\n\n#include <variant>\n\n#include \"utl/verify.h\"\n\n#include \"osr/location.h\"\n#include \"osr/platforms.h\"\n\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/special_stations.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"motis/parse_location.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/clasz_to_mode.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\ntt_location::tt_location(nigiri::rt::run_stop const& stop)\n    : l_{stop.get_location_idx()},\n      scheduled_{stop.get_scheduled_location_idx()} {}\n\ntt_location::tt_location(nigiri::location_idx_t const l,\n                         nigiri::location_idx_t const scheduled)\n    : l_{l},\n      scheduled_{scheduled == n::location_idx_t::invalid() ? l : scheduled} {}\n\napi::Place to_place(osr::location const l,\n                    std::string_view name,\n                    std::optional<std::string> const& tz) {\n  return {\n      .name_ = std::string{name},\n      .lat_ = l.pos_.lat_,\n      .lon_ = l.pos_.lng_,\n      .level_ = l.lvl_.to_float(),\n      .tz_ = tz,\n      .vertexType_ = api::VertexTypeEnum::NORMAL,\n  };\n}\n\nosr::level_t get_lvl(osr::ways const* w,\n                     osr::platforms const* pl,\n                     platform_matches_t const* matches,\n                     n::location_idx_t const l) {\n  return w && pl && matches ? pl->get_level(*w, (*matches).at(l))\n                            : osr::kNoLevel;\n}\n\ndouble get_level(osr::ways const* w,\n                 osr::platforms const* pl,\n                 platform_matches_t const* matches,\n                 n::location_idx_t const l) {\n  return get_lvl(w, pl, matches, l).to_float();\n}\n\nosr::location get_location(api::Place const& p) {\n  return {{p.lat_, p.lon_}, osr::level_t{static_cast<float>(p.level_)}};\n}\n\nosr::location get_location(n::timetable const* tt,\n                           osr::ways const* w,\n                           osr::platforms const* pl,\n                           platform_matches_t const* matches,\n                           place_t const loc,\n                           place_t const start,\n                           place_t const dest) {\n  return std::visit(\n      utl::overloaded{\n          [&](osr::location const& l) { return l; },\n          [&](tt_location const l) {\n            auto l_idx = l.l_;\n            if (l_idx == static_cast<n::location_idx_t::value_t>(\n                             n::special_station::kStart)) {\n              if (std::holds_alternative<osr::location>(start)) {\n                return std::get<osr::location>(start);\n              }\n              l_idx = std::get<tt_location>(start).l_;\n            } else if (l_idx == static_cast<n::location_idx_t::value_t>(\n                                    n::special_station::kEnd)) {\n              if (std::holds_alternative<osr::location>(dest)) {\n                return std::get<osr::location>(dest);\n              }\n              l_idx = std::get<tt_location>(dest).l_;\n            }\n            utl::verify(tt != nullptr,\n                        \"resolving stop coordinates: timetable not set\");\n            return osr::location{tt->locations_.coordinates_.at(l_idx),\n                                 get_lvl(w, pl, matches, l_idx)};\n          }},\n      loc);\n}\n\napi::Place to_place(n::timetable const* tt,\n                    tag_lookup const* tags,\n                    osr::ways const* w,\n                    osr::platforms const* pl,\n                    platform_matches_t const* matches,\n                    adr_ext const* ae,\n                    tz_map_t const* tz_map,\n                    n::lang_t const& lang,\n                    place_t const l,\n                    place_t const start,\n                    place_t const dest,\n                    std::string_view name,\n                    std::optional<std::string> const& fallback_tz) {\n  return std::visit(\n      utl::overloaded{\n          [&](osr::location const& l) {\n            return to_place(l, name, fallback_tz);\n          },\n          [&](tt_location const tt_l) -> api::Place {\n            utl::verify(tt && tags, \"resolving stops requires timetable\");\n\n            auto l = tt_l.l_;\n            if (l == n::get_special_station(n::special_station::kStart)) {\n              if (std::holds_alternative<osr::location>(start)) {\n                return to_place(std::get<osr::location>(start), \"START\",\n                                fallback_tz);\n              }\n              l = std::get<tt_location>(start).l_;\n            } else if (l == n::get_special_station(n::special_station::kEnd)) {\n              if (std::holds_alternative<osr::location>(dest)) {\n                return to_place(std::get<osr::location>(dest), \"END\",\n                                fallback_tz);\n              }\n              l = std::get<tt_location>(dest).l_;\n            }\n            auto const get_track = [&](n::location_idx_t const x) {\n              auto const p =\n                  tt->translate(lang, tt->locations_.platform_codes_.at(x));\n              return p.empty() ? std::nullopt : std::optional{std::string{p}};\n            };\n\n            // check if description is available, if not, return nullopt\n            auto const get_description = [&](n::location_idx_t const x) {\n              auto const p =\n                  tt->translate(lang, tt->locations_.descriptions_.at(x));\n              return p.empty() ? std::nullopt : std::optional{std::string{p}};\n            };\n\n            auto const pos = tt->locations_.coordinates_[l];\n            auto const p = tt->locations_.get_root_idx(l);\n            auto const timezone = get_tz(*tt, ae, tz_map, p);\n\n            return {\n                .name_ = std::string{tt->translate(\n                    lang, tt->locations_.names_.at(p))},\n                .stopId_ = tags->id(*tt, l),\n                .parentId_ = p == n::location_idx_t::invalid() || p == l\n                                 ? std::nullopt\n                                 : std::optional{tags->id(*tt, p)},\n                .importance_ = ae == nullptr\n                                   ? std::nullopt\n                                   : std::optional{ae->place_importance_.at(\n                                         ae->location_place_.at(l))},\n                .lat_ = pos.lat_,\n                .lon_ = pos.lng_,\n                .level_ = get_level(w, pl, matches, l),\n                .tz_ = timezone == nullptr ? fallback_tz\n                                           : std::optional{timezone->name()},\n                .scheduledTrack_ = get_track(tt_l.scheduled_),\n                .track_ = get_track(tt_l.l_),\n                .description_ = get_description(tt_l.scheduled_),\n                .vertexType_ = api::VertexTypeEnum::TRANSIT,\n                .modes_ =\n                    ae != nullptr\n                        ? std::optional<std::vector<api::ModeEnum>>{to_modes(\n                              ae->place_clasz_.at(ae->location_place_.at(p)),\n                              5)}\n                        : std::nullopt};\n          }},\n      l);\n}\n\napi::Place to_place(n::timetable const* tt,\n                    tag_lookup const* tags,\n                    osr::ways const* w,\n                    osr::platforms const* pl,\n                    platform_matches_t const* matches,\n                    adr_ext const* ae,\n                    tz_map_t const* tz_map,\n                    n::lang_t const& lang,\n                    n::rt::run_stop const& s,\n                    place_t const start,\n                    place_t const dest) {\n  auto const run_cancelled = s.fr_->is_cancelled();\n  auto const fallback_tz = s.get_tz_name(\n      s.stop_idx_ == 0 ? n::event_type::kDep : n::event_type::kArr);\n  auto p = to_place(tt, tags, w, pl, matches, ae, tz_map, lang, tt_location{s},\n                    start, dest, \"\", fallback_tz);\n  p.pickupType_ = !run_cancelled && s.in_allowed()\n                      ? api::PickupDropoffTypeEnum::NORMAL\n                      : api::PickupDropoffTypeEnum::NOT_ALLOWED;\n  p.dropoffType_ = !run_cancelled && s.out_allowed()\n                       ? api::PickupDropoffTypeEnum::NORMAL\n                       : api::PickupDropoffTypeEnum::NOT_ALLOWED;\n  p.cancelled_ = run_cancelled || (!s.in_allowed() && !s.out_allowed() &&\n                                   (s.get_scheduled_stop().in_allowed() ||\n                                    s.get_scheduled_stop().out_allowed()));\n  return p;\n}\n\nplace_t get_place(n::timetable const* tt,\n                  tag_lookup const* tags,\n                  std::string_view input) {\n  if (auto const location = parse_location(input); location.has_value()) {\n    return *location;\n  }\n  utl::verify(tt != nullptr && tags != nullptr,\n              R\"(could not parse location (no timetable loaded): \"{}\")\", input);\n  return tt_location{tags->get_location(*tt, input)};\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/polyline.cc",
    "content": "#include \"motis/polyline.h\"\n\n#include \"geo/polyline_format.h\"\n\nnamespace motis {\n\ntemplate <std::int64_t Precision>\napi::EncodedPolyline to_polyline(geo::polyline const& polyline) {\n  return {geo::encode_polyline<Precision>(polyline), Precision,\n          static_cast<std::int64_t>(polyline.size())};\n}\n\napi::EncodedPolyline empty_polyline() {\n  return api::EncodedPolyline{.points_ = \"\", .precision_ = 6, .length_ = 0};\n}\n\ntemplate api::EncodedPolyline to_polyline<5>(geo::polyline const&);\ntemplate api::EncodedPolyline to_polyline<6>(geo::polyline const&);\ntemplate api::EncodedPolyline to_polyline<7>(geo::polyline const&);\n\n}  // namespace motis"
  },
  {
    "path": "src/railviz.cc",
    "content": "#include \"motis/railviz.h\"\n\n#include <algorithm>\n#include <bit>\n#include <cstddef>\n#include <optional>\n#include <ranges>\n#include <string_view>\n#include <type_traits>\n\n#include \"cista/containers/rtree.h\"\n#include \"cista/reflection/comparable.h\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/not_found_exception.h\"\n#include \"net/too_many_exception.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/get_or_create.h\"\n#include \"utl/helpers/algorithm.h\"\n#include \"utl/pairwise.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/verify.h\"\n\n#include \"geo/box.h\"\n#include \"geo/detail/register_box.h\"\n#include \"geo/latlng.h\"\n#include \"geo/polyline_format.h\"\n\n#include \"nigiri/common/interval.h\"\n#include \"nigiri/common/linear_lower_bound.h\"\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n#include \"nigiri/rt/run.h\"\n#include \"nigiri/shapes_storage.h\"\n#include \"nigiri/timetable.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/data.h\"\n#include \"motis/journey_to_response.h\"\n#include \"motis/parse_location.h\"\n#include \"motis/place.h\"\n#include \"motis/tag_lookup.h\"\n#include \"motis/timetable/clasz_to_mode.h\"\n#include \"motis/timetable/time_conv.h\"\n\nnamespace n = nigiri;\n\nconstexpr auto const kTripsLimit = 12'000U;\nconstexpr auto const kRoutesLimit = 20'000U;\nconstexpr auto const kRoutesPolylinesLimit = 20'000U;\n\nusing static_rtree = cista::raw::rtree<n::route_idx_t>;\nusing rt_rtree = cista::raw::rtree<n::rt_transport_idx_t>;\n\nusing int_clasz = std::underlying_type_t<n::clasz>;\n\nnamespace motis {\n\nstruct stop_pair {\n  n::rt::run r_;\n  n::stop_idx_t from_{}, to_{};\n};\n\nint min_zoom_level(n::clasz const clasz, float const distance) {\n  switch (clasz) {\n    // long distance\n    case n::clasz::kAir:\n    case n::clasz::kCoach:\n      if (distance < 50'000.F) {\n        return 8;  // typically long distance, maybe also quite short\n      }\n      [[fallthrough]];\n    case n::clasz::kHighSpeed:\n    case n::clasz::kLongDistance:\n    case n::clasz::kNight: return 4;\n    case n::clasz::kRideSharing:\n    case n::clasz::kRegional: return 7;\n\n    // regional distance\n    case n::clasz::kSuburban: return 8;\n\n    // metro distance\n    case n::clasz::kSubway: return 9;\n\n    // short distance\n    case n::clasz::kTram:\n    case n::clasz::kBus: return distance > 10'000.F ? 9 : 10;\n\n    // ship can be anything\n    case n::clasz::kShip:\n      if (distance > 100'000.F) {\n        return 5;\n      } else if (distance > 10'000.F) {\n        return 8;\n      } else {\n        return 10;\n      }\n\n    case n::clasz::kODM:\n    case n::clasz::kFunicular:\n    case n::clasz::kAerialLift:\n    case n::clasz::kOther: return 11;\n\n    default: throw utl::fail(\"unknown n::clasz {}\", static_cast<int>(clasz));\n  }\n}\n\nbool should_display(n::clasz const clasz,\n                    int const zoom_level,\n                    float const distance) {\n  return zoom_level >= min_zoom_level(clasz, distance);\n}\n\nstruct route_geo_index {\n  route_geo_index() = default;\n\n  route_geo_index(n::timetable const& tt,\n                  n::shapes_storage const* shapes_data,\n                  n::clasz const clasz,\n                  n::vector_map<n::route_idx_t, float>& distances) {\n    // Fallback, if no route bounding box can be loaded\n    auto const get_box = [&](n::route_idx_t const route_idx) {\n      auto bounding_box = geo::box{};\n      for (auto const l : tt.route_location_seq_[route_idx]) {\n        bounding_box.extend(\n            tt.locations_.coordinates_.at(n::stop{l}.location_idx()));\n      }\n      return bounding_box;\n    };\n    for (auto const [i, claszes] : utl::enumerate(tt.route_section_clasz_)) {\n      auto const r = n::route_idx_t{i};\n      if (claszes.at(0) != clasz) {\n        continue;\n      }\n      auto const bounding_box = (shapes_data == nullptr)\n                                    ? get_box(r)\n                                    : shapes_data->get_bounding_box(r);\n\n      rtree_.insert(bounding_box.min_.lnglat_float(),\n                    bounding_box.max_.lnglat_float(), r);\n      distances[r] = static_cast<float>(\n          geo::distance(bounding_box.max_, bounding_box.min_));\n    }\n  }\n\n  std::vector<n::route_idx_t> get_routes(geo::box const& b) const {\n    auto routes = std::vector<n::route_idx_t>{};\n    rtree_.search(b.min_.lnglat_float(), b.max_.lnglat_float(),\n                  [&](auto, auto, n::route_idx_t const r) {\n                    routes.push_back(r);\n                    return true;\n                  });\n    return routes;\n  }\n\n  static_rtree rtree_{};\n};\n\nstruct rt_transport_geo_index {\n  rt_transport_geo_index() = default;\n\n  rt_transport_geo_index(\n      n::timetable const& tt,\n      n::rt_timetable const& rtt,\n      n::clasz const clasz,\n      n::vector_map<n::rt_transport_idx_t, float>& distances) {\n    for (auto const [i, claszes] :\n         utl::enumerate(rtt.rt_transport_section_clasz_)) {\n      auto const rt_t = n::rt_transport_idx_t{i};\n      if (claszes.at(0) != clasz) {\n        continue;\n      }\n\n      auto bounding_box = geo::box{};\n      for (auto const l : rtt.rt_transport_location_seq_[rt_t]) {\n        bounding_box.extend(\n            tt.locations_.coordinates_.at(n::stop{l}.location_idx()));\n      }\n\n      rtree_.insert(bounding_box.min_.lnglat_float(),\n                    bounding_box.max_.lnglat_float(), rt_t);\n      distances[rt_t] = static_cast<float>(\n          geo::distance(bounding_box.min_, bounding_box.max_));\n    }\n  }\n\n  std::vector<n::rt_transport_idx_t> get_rt_transports(\n      n::rt_timetable const& rtt, geo::box const& b) const {\n    auto rt_transports = std::vector<n::rt_transport_idx_t>{};\n    rtree_.search(b.min_.lnglat_float(), b.max_.lnglat_float(),\n                  [&](auto, auto, n::rt_transport_idx_t const rt_t) {\n                    if (!rtt.rt_transport_is_cancelled_[to_idx(rt_t)]) {\n                      rt_transports.emplace_back(rt_t);\n                    }\n                    return true;\n                  });\n    return rt_transports;\n  }\n\n  rt_rtree rtree_{};\n};\n\nstruct railviz_static_index::impl {\n  std::array<route_geo_index, n::kNumClasses> static_geo_indices_;\n  n::vector_map<n::route_idx_t, float> static_distances_{};\n};\n\nrailviz_static_index::railviz_static_index(n::timetable const& tt,\n                                           n::shapes_storage const* shapes_data)\n    : impl_{std::make_unique<impl>()} {\n  impl_->static_distances_.resize(tt.route_location_seq_.size());\n  for (auto c = int_clasz{0U}; c != n::kNumClasses; ++c) {\n    impl_->static_geo_indices_[c] =\n        route_geo_index{tt, shapes_data, n::clasz{c}, impl_->static_distances_};\n  }\n}\n\nrailviz_static_index::~railviz_static_index() = default;\n\nstruct railviz_rt_index::impl {\n  std::array<rt_transport_geo_index, n::kNumClasses> rt_geo_indices_;\n  n::vector_map<n::rt_transport_idx_t, float> rt_distances_{};\n};\n\nrailviz_rt_index::railviz_rt_index(nigiri::timetable const& tt,\n                                   nigiri::rt_timetable const& rtt)\n    : impl_{std::make_unique<impl>()} {\n  impl_->rt_distances_.resize(rtt.rt_transport_location_seq_.size());\n  for (auto c = int_clasz{0U}; c != n::kNumClasses; ++c) {\n    impl_->rt_geo_indices_[c] =\n        rt_transport_geo_index{tt, rtt, n::clasz{c}, impl_->rt_distances_};\n  }\n}\n\nrailviz_rt_index::~railviz_rt_index() = default;\n\nvoid add_rt_transports(n::timetable const& tt,\n                       n::rt_timetable const& rtt,\n                       n::rt_transport_idx_t const rt_t,\n                       n::interval<n::unixtime_t> const time_interval,\n                       geo::box const& area,\n                       std::vector<stop_pair>& runs) {\n  auto const fr = n::rt::frun::from_rt(tt, &rtt, rt_t);\n  for (auto const [from, to] : utl::pairwise(fr)) {\n    auto const box = geo::make_box({from.pos(), to.pos()});\n    if (!box.overlaps(area)) {\n      continue;\n    }\n\n    auto const active =\n        n::interval{from.time(n::event_type::kDep),\n                    to.time(n::event_type::kArr) + n::i32_minutes{1}};\n    if (active.overlaps(time_interval)) {\n      utl::verify<net::too_many_exception>(runs.size() < kTripsLimit,\n                                           \"too many trips\");\n      runs.emplace_back(\n          stop_pair{.r_ = fr,  // NOLINT(cppcoreguidelines-slicing)\n                    .from_ = from.stop_idx_,\n                    .to_ = to.stop_idx_});\n    }\n  }\n}\n\nvoid add_static_transports(n::timetable const& tt,\n                           n::rt_timetable const* rtt,\n                           n::route_idx_t const r,\n                           n::interval<n::unixtime_t> const time_interval,\n                           geo::box const& area,\n                           n::shapes_storage const* shapes_data,\n                           std::vector<stop_pair>& runs) {\n  auto const is_active = [&](n::transport const t) -> bool {\n    return (rtt == nullptr\n                ? tt.bitfields_[tt.transport_traffic_days_[t.t_idx_]]\n                : rtt->bitfields_[rtt->transport_traffic_days_[t.t_idx_]])\n        .test(to_idx(t.day_));\n  };\n\n  auto const seq = tt.route_location_seq_[r];\n  auto const stop_indices =\n      n::interval{n::stop_idx_t{0U}, static_cast<n::stop_idx_t>(seq.size())};\n  auto const [start_day, _] = tt.day_idx_mam(time_interval.from_);\n  auto const [end_day, _1] = tt.day_idx_mam(time_interval.to_);\n  auto const get_box = [&](std::size_t segment) {\n    if (shapes_data != nullptr) {\n      auto const box = shapes_data->get_bounding_box(r, segment);\n      if (box.has_value()) {\n        return *box;\n      }\n    }\n    // Fallback, if no segment bounding box can be loaded\n    return geo::make_box(\n        {tt.locations_.coordinates_[n::stop{seq[segment]}.location_idx()],\n         tt.locations_.coordinates_[n::stop{seq[segment + 1]}.location_idx()]});\n  };\n  for (auto const [from, to] : utl::pairwise(stop_indices)) {\n    // TODO dwell times\n    auto const box = get_box(from);\n    if (!box.overlaps(area)) {\n      continue;\n    }\n\n    auto const arr_times = tt.event_times_at_stop(r, to, n::event_type::kArr);\n    for (auto const [i, t_idx] :\n         utl::enumerate(tt.route_transport_ranges_[r])) {\n      auto const day_offset =\n          static_cast<n::day_idx_t::value_t>(arr_times[i].days());\n      for (auto traffic_day = start_day - day_offset; traffic_day <= end_day;\n           ++traffic_day) {\n        auto const t = n::transport{t_idx, traffic_day};\n        if (time_interval.overlaps({tt.event_time(t, from, n::event_type::kDep),\n                                    tt.event_time(t, to, n::event_type::kArr) +\n                                        n::unixtime_t::duration{1}}) &&\n            is_active(t)) {\n          utl::verify<net::too_many_exception>(runs.size() < kTripsLimit,\n                                               \"too many trips\");\n          runs.emplace_back(stop_pair{\n              .r_ = n::rt::run{.t_ = t,\n                               .stop_range_ = {from, static_cast<n::stop_idx_t>(\n                                                         to + 1U)},\n                               .rt_ = n::rt_transport_idx_t::invalid()},\n              .from_ = 0,\n              .to_ = 1});\n        }\n      }\n    }\n  }\n}\n\napi::trips_response get_trains(tag_lookup const& tags,\n                               n::timetable const& tt,\n                               n::rt_timetable const* rtt,\n                               n::shapes_storage const* shapes,\n                               osr::ways const* w,\n                               osr::platforms const* pl,\n                               platform_matches_t const* matches,\n                               adr_ext const* ae,\n                               tz_map_t const* tz,\n                               railviz_static_index::impl const& static_index,\n                               railviz_rt_index::impl const& rt_index,\n                               api::trips_params const& query,\n                               unsigned const api_version) {\n  // Parse query.\n  auto const zoom_level = static_cast<int>(query.zoom_);\n  auto const min = parse_location(query.min_);\n  auto const max = parse_location(query.max_);\n  utl::verify(min.has_value(), \"min not a coordinate: {}\", query.min_);\n  utl::verify(max.has_value(), \"max not a coordinate: {}\", query.max_);\n  auto const start_time =\n      std::chrono::time_point_cast<n::unixtime_t::duration>(*query.startTime_);\n  auto const end_time =\n      std::chrono::time_point_cast<n::unixtime_t::duration>(*query.endTime_);\n  auto const time_interval = n::interval{start_time, end_time};\n  auto const area = geo::make_box({min->pos_, max->pos_});\n\n  // Collect runs within time+location window.\n  auto runs = std::vector<stop_pair>{};\n  for (auto c = int_clasz{0U}; c != n::kNumClasses; ++c) {\n    auto const cl = n::clasz{c};\n    if (!should_display(cl, zoom_level,\n                        std::numeric_limits<float>::infinity())) {\n      continue;\n    }\n\n    if (rtt != nullptr) {\n      for (auto const& rt_t :\n           rt_index.rt_geo_indices_[c].get_rt_transports(*rtt, area)) {\n        if (should_display(cl, zoom_level, rt_index.rt_distances_[rt_t])) {\n          add_rt_transports(tt, *rtt, rt_t, time_interval, area, runs);\n        }\n      }\n    }\n\n    for (auto const& r : static_index.static_geo_indices_[c].get_routes(area)) {\n      if (should_display(cl, zoom_level, static_index.static_distances_[r])) {\n        add_static_transports(tt, rtt, r, time_interval, area, shapes, runs);\n      }\n    }\n  }\n\n  auto const precision = static_cast<std::int8_t>(query.precision_);\n  utl::verify<net::bad_request_exception>(\n      precision >= 0 && precision < 7,\n      \"invalid precision for polylines, allowed are [0, 6]\");\n  return geo::with_polyline_encoder(precision, [&](auto enc) mutable {\n    return utl::to_vec(runs, [&](stop_pair const& r) -> api::TripSegment {\n      enc.reset();\n\n      auto const fr = n::rt::frun{tt, rtt, r.r_};\n\n      auto const from = fr[r.from_];\n      auto const to = fr[r.to_];\n\n      fr.for_each_shape_point(shapes,\n                              {r.from_, static_cast<n::stop_idx_t>(r.to_ + 1U)},\n                              [&](auto&& p) { enc.push_nonzero_diff(p, 2); });\n\n      return {\n          .trips_ = {api::TripInfo{\n              .tripId_ = tags.id(tt, from, n::event_type::kDep),\n              .routeShortName_ =\n                  api_version < 4 ? std::optional{std::string{from.display_name(\n                                        n::event_type::kDep, query.language_)}}\n                                  : std::nullopt,\n              .displayName_ = api_version >= 4\n                                  ? std::optional{std::string{from.display_name(\n                                        n::event_type::kDep, query.language_)}}\n                                  : std::nullopt}},\n          .routeColor_ =\n              to_str(from.get_route_color(n::event_type::kDep).color_),\n          .mode_ = to_mode(from.get_clasz(n::event_type::kDep), api_version),\n          .distance_ =\n              fr.is_rt()\n                  ? rt_index.rt_distances_[fr.rt_]\n                  : static_index\n                        .static_distances_[tt.transport_route_[fr.t_.t_idx_]],\n          .from_ = to_place(&tt, &tags, w, pl, matches, ae, tz, query.language_,\n                            tt_location{from}),\n          .to_ = to_place(&tt, &tags, w, pl, matches, ae, tz, query.language_,\n                          tt_location{to}),\n          .departure_ = from.time(n::event_type::kDep),\n          .arrival_ = to.time(n::event_type::kArr),\n          .scheduledDeparture_ = from.scheduled_time(n::event_type::kDep),\n          .scheduledArrival_ = to.scheduled_time(n::event_type::kArr),\n          .realTime_ = fr.is_rt(),\n          .polyline_ = std::move(enc.buf_)};\n    });\n  });\n}\n\nstruct route_info {\n  CISTA_COMPARABLE()\n\n  std::string_view id_;\n  std::string_view short_name_;\n  std::string_view long_name_;\n  nigiri::route_color color_;\n};\n\ntemplate <typename Response>\nResponse build_routes_response(\n    tag_lookup const& tags,\n    n::timetable const& tt,\n    n::shapes_storage const* shapes,\n    osr::ways const* w,\n    osr::platforms const* pl,\n    platform_matches_t const* matches,\n    std::vector<n::route_idx_t> const& route_indexes,\n    std::optional<geo::box> const& area,\n    std::optional<std::vector<std::string>> const& language) {\n  auto res = Response{};\n  auto enc = geo::polyline_encoder<6>{};\n\n  auto const add_unique = [](auto& values, auto const& value) {\n    if (utl::find(values, value) == end(values)) {\n      values.emplace_back(value);\n    }\n  };\n\n  auto stop_indexes = std::vector<std::int64_t>{};\n  stop_indexes.resize(tt.locations_.coordinates_.size(), -1);\n  auto const get_stop_index = [&](n::rt::run_stop const& stop) {\n    auto const l = stop.get_location_idx();\n    auto& stop_index = stop_indexes[to_idx(l)];\n    if (stop_index == -1) {\n      auto const parent = tt.locations_.get_root_idx(l);\n      auto const root = tt.locations_.get_root_idx(l);\n      auto const pos = tt.locations_.coordinates_.at(l);\n      stop_index = static_cast<std::int64_t>(res.stops_.size());\n      res.stops_.emplace_back(\n          api::Place{.name_ = std::string{tt.translate(\n                         language, tt.locations_.names_.at(root))},\n                     .stopId_ = tags.id(tt, l),\n                     .parentId_ = parent == n::location_idx_t::invalid()\n                                      ? std::nullopt\n                                      : std::optional{tags.id(tt, parent)},\n                     .lat_ = pos.lat_,\n                     .lon_ = pos.lng_,\n                     .level_ = get_lvl(w, pl, matches, l).to_float()});\n    }\n    return stop_index;\n  };\n\n  auto polyline_indexes = hash_map<std::string, std::int64_t>{};\n  auto const get_polyline_index = [&](std::string points,\n                                      std::int64_t const length) {\n    auto const new_index = static_cast<std::int64_t>(res.polylines_.size());\n    auto const [it, inserted] = polyline_indexes.try_emplace(points, new_index);\n    if (inserted) {\n      utl::verify<net::too_many_exception>(\n          res.polylines_.size() < kRoutesPolylinesLimit, \"too many polylines\");\n      res.polylines_.emplace_back(api::RoutePolyline{\n          .polyline_ = api::EncodedPolyline{.points_ = std::move(points),\n                                            .precision_ = 6,\n                                            .length_ = length},\n          .colors_ = {},\n          .routeIndexes_ = {}});\n      return new_index;\n    }\n    return it->second;\n  };\n\n  for (auto const r : route_indexes) {\n    utl::verify<net::too_many_exception>(res.routes_.size() < kRoutesLimit,\n                                         \"too many routes\");\n\n    auto route_segments = std::vector<api::RouteSegment>{};\n    auto route_polyline_indexes = std::vector<std::int64_t>{};\n    auto route_infos = hash_set<route_info>{};\n    auto const stops = tt.route_location_seq_[r];\n    auto shape_added = false;\n\n    auto const get_box = [&](std::size_t segment) {\n      if (shapes != nullptr) {\n        auto const box = shapes->get_bounding_box(r, segment);\n        if (box.has_value()) {\n          return *box;\n        }\n      }\n      // Fallback, if no segment bounding box can be loaded\n      return geo::make_box(\n          {tt.locations_.coordinates_[n::stop{stops[segment]}.location_idx()],\n           tt.locations_\n               .coordinates_[n::stop{stops[segment + 1U]}.location_idx()]});\n    };\n\n    auto path_source = api::RoutePathSourceEnum::NONE;\n    auto const cl = tt.route_clasz_[r];\n\n    for (auto const transport_idx : tt.route_transport_ranges_[r]) {\n      auto const stop_indices = n::interval{\n          n::stop_idx_t{0U}, static_cast<n::stop_idx_t>(stops.size())};\n      for (auto const [from, to] : utl::pairwise(stop_indices)) {\n        enc.reset();\n        auto n_points = 0;\n\n        auto const fr = n::rt::frun{\n            tt, nullptr,\n            n::rt::run{\n                .t_ = n::transport{transport_idx, n::day_idx_t{0}},\n                .stop_range_ =\n                    n::interval{from, static_cast<n::stop_idx_t>(to + 1U)},\n                .rt_ = n::rt_transport_idx_t::invalid()}};\n        if (from == stop_indices.from_) {\n          route_infos.emplace(route_info{\n              .id_ = fr[0].get_route_id(n::event_type::kDep),\n              .short_name_ =\n                  fr[0].route_short_name(n::event_type::kDep, language),\n              .long_name_ =\n                  fr[0].route_long_name(n::event_type::kDep, language),\n              .color_ = fr[0].get_route_color(n::event_type::kDep)});\n        }\n\n        if (shape_added ||\n            (area.has_value() && !get_box(from).overlaps(*area))) {\n          continue;\n        }\n\n        fr.for_each_shape_point(\n            shapes, n::interval{n::stop_idx_t{0U}, n::stop_idx_t{2U}},\n            [&](auto&& p) {\n              enc.push(p);\n              ++n_points;\n            });\n        if (fr.is_scheduled()) {\n          auto const trp_idx = fr.trip_idx();\n          auto const shp_idx = shapes->get_shape_idx(trp_idx);\n          if (shp_idx != n::scoped_shape_idx_t::invalid()) {\n            switch (n::get_shape_source(shp_idx)) {\n              case n::shape_source::kNone:\n                path_source = api::RoutePathSourceEnum::NONE;\n                break;\n              case n::shape_source::kTimetable:\n                path_source = api::RoutePathSourceEnum::TIMETABLE;\n                break;\n              case n::shape_source::kRouted:\n                path_source = api::RoutePathSourceEnum::ROUTED;\n                break;\n            }\n          }\n        }\n\n        route_segments.emplace_back(api::RouteSegment{\n            .from_ = get_stop_index(fr[0]),\n            .to_ = get_stop_index(fr[1]),\n            .polyline_ = get_polyline_index(std::move(enc.buf_), n_points),\n        });\n        add_unique(route_polyline_indexes, route_segments.back().polyline_);\n      }\n      shape_added = true;\n    }\n\n    auto route_colors = std::vector<std::string>{};\n    for (auto const& ri : route_infos) {\n      if (auto const color = to_str(ri.color_.color_); color.has_value()) {\n        add_unique(route_colors, *color);\n      }\n    }\n\n    auto const route_index = static_cast<std::int64_t>(res.routes_.size());\n    for (auto const polyline_index : route_polyline_indexes) {\n      auto& polyline = res.polylines_[static_cast<std::size_t>(polyline_index)];\n      add_unique(polyline.routeIndexes_, route_index);\n      for (auto const& color : route_colors) {\n        add_unique(polyline.colors_, color);\n      }\n    }\n\n    res.routes_.emplace_back(api::RouteInfo{\n        .mode_ = to_mode(cl, 5),\n        .transitRoutes_ =\n            utl::to_vec(route_infos,\n                        [&](auto const& ri) {\n                          return api::TransitRouteInfo{\n                              .id_ = std::string{ri.id_},\n                              .shortName_ = std::string{ri.short_name_},\n                              .longName_ = std::string{ri.long_name_},\n                              .color_ = to_str(ri.color_.color_),\n                              .textColor_ = to_str(ri.color_.text_color_)};\n                        }),\n        .numStops_ = static_cast<std::int64_t>(stops.size()),\n        .routeIdx_ = to_idx(r),\n        .pathSource_ = path_source,\n        .segments_ = std::move(route_segments),\n    });\n  }\n\n  return res;\n}\n\napi::routes_response get_routes(tag_lookup const& tags,\n                                n::timetable const& tt,\n                                n::rt_timetable const*,\n                                n::shapes_storage const* shapes,\n                                osr::ways const* w,\n                                osr::platforms const* pl,\n                                platform_matches_t const* matches,\n                                adr_ext const*,\n                                tz_map_t const*,\n                                railviz_static_index::impl const& static_index,\n                                railviz_rt_index::impl const&,\n                                api::routes_params const& query,\n                                unsigned const /*api_version*/) {\n  auto const zoom_level = static_cast<int>(query.zoom_);\n  auto const min = parse_location(query.min_);\n  auto const max = parse_location(query.max_);\n  utl::verify(min.has_value(), \"min not a coordinate: {}\", query.min_);\n  utl::verify(max.has_value(), \"max not a coordinate: {}\", query.max_);\n  auto const area = geo::make_box({min->pos_, max->pos_});\n  auto route_indexes = std::vector<n::route_idx_t>{};\n  auto zoom_filtered = false;\n\n  static_assert(static_cast<int_clasz>(n::clasz::kAir) == 0U);  // skip air\n  for (auto c = int_clasz{1U}; c != n::kNumClasses; ++c) {\n    auto const cl = n::clasz{c};\n    if (!should_display(cl, zoom_level,\n                        std::numeric_limits<float>::infinity())) {\n      zoom_filtered = true;\n      continue;\n    }\n\n    for (auto const& r : static_index.static_geo_indices_[c].get_routes(area)) {\n      if (should_display(cl, zoom_level, static_index.static_distances_[r])) {\n        route_indexes.emplace_back(r);\n      } else {\n        zoom_filtered = true;\n      }\n    }\n  }\n\n  auto res = build_routes_response<api::routes_response>(\n      tags, tt, shapes, w, pl, matches, route_indexes, area, query.language_);\n  res.zoomFiltered_ = zoom_filtered;\n  return res;\n}\n\napi::routeDetails_response get_route_details(\n    tag_lookup const& tags,\n    n::timetable const& tt,\n    n::rt_timetable const*,\n    n::shapes_storage const* shapes,\n    osr::ways const* w,\n    osr::platforms const* pl,\n    platform_matches_t const* matches,\n    adr_ext const*,\n    tz_map_t const*,\n    railviz_static_index::impl const&,\n    railviz_rt_index::impl const&,\n    api::routeDetails_params const& query,\n    unsigned const /*api_version*/) {\n  utl::verify<net::bad_request_exception>(\n      query.routeIdx_ >= 0 &&\n          query.routeIdx_ < static_cast<std::int64_t>(tt.n_routes()),\n      \"invalid route index {}\", query.routeIdx_);\n\n  auto const route =\n      n::route_idx_t{static_cast<n::route_idx_t::value_t>(query.routeIdx_)};\n\n  auto res = build_routes_response<api::routeDetails_response>(\n      tags, tt, shapes, w, pl, matches, {route}, std::nullopt, query.language_);\n  res.zoomFiltered_ = false;\n  return res;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/route_shapes.cc",
    "content": "#include \"motis/route_shapes.h\"\n\n#include <algorithm>\n#include <chrono>\n#include <iostream>\n#include <mutex>\n#include <optional>\n#include <ranges>\n#include <set>\n#include <string_view>\n#include <vector>\n\n#include \"boost/stacktrace.hpp\"\n\n#include \"cista/hashing.h\"\n#include \"cista/serialization.h\"\n\n#include \"utl/parallel_for.h\"\n#include \"utl/progress_tracker.h\"\n#include \"utl/sorted_diff.h\"\n#include \"utl/to_vec.h\"\n#include \"utl/verify.h\"\n\n#include \"geo/box.h\"\n#include \"geo/latlng.h\"\n\n#include \"nigiri/loader/build_lb_graph.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/shapes_storage.h\"\n#include \"nigiri/timetable.h\"\n\n#include \"osr/routing/map_matching.h\"\n#include \"osr/routing/map_matching_debug.h\"\n#include \"osr/routing/parameters.h\"\n#include \"osr/routing/profile.h\"\n#include \"osr/routing/route.h\"\n#include \"osr/types.h\"\n\n#include \"motis/match_platforms.h\"\n#include \"motis/types.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nstd::string_view to_string_view(cista::byte_buf const& data) {\n  return {reinterpret_cast<char const*>(data.data()), data.size()};\n}\n\nstruct shape_cache_payload {\n  shape_cache_key key_;\n  shape_cache_entry entry_;\n};\n\nusing shape_cache_bucket = cista::offset::vector<shape_cache_payload>;\n\nshape_cache::shape_cache(std::filesystem::path const& path,\n                         mdb_size_t const map_size)\n    : last_sync_{std::chrono::steady_clock::now()} {\n  env_.set_mapsize(map_size);\n  env_.set_maxdbs(1);\n  env_.open(path.generic_string().c_str(),\n            lmdb::env_open_flags::NOSUBDIR | lmdb::env_open_flags::NOSYNC);\n\n  auto txn = lmdb::txn{env_};\n  txn.dbi_open(lmdb::dbi_flags::CREATE);\n  txn.commit();\n}\n\nshape_cache::~shape_cache() { sync(); }\n\nstd::optional<shape_cache_entry> shape_cache::get(shape_cache_key const& key) {\n  auto txn = lmdb::txn{env_, lmdb::txn_flags::RDONLY};\n  auto dbi = txn.dbi_open();\n  auto const bucket = cista::build_hash(key);\n  auto const value = txn.get(dbi, bucket);\n  if (!value.has_value()) {\n    return std::nullopt;\n  }\n\n  auto const entries =\n      cista::deserialize<shape_cache_bucket, cista::mode::CAST>(*value);\n  for (auto const& payload : *entries) {\n    if (payload.key_ == key) {\n      return payload.entry_;\n    }\n  }\n\n  return std::nullopt;\n}\n\nvoid shape_cache::put(shape_cache_key const& key,\n                      shape_cache_entry const& entry) {\n  auto txn = lmdb::txn{env_};\n  auto dbi = txn.dbi_open();\n  auto const bucket_key = cista::build_hash(key);\n  auto entries = shape_cache_bucket{};\n\n  if (auto const value = txn.get(dbi, bucket_key); value.has_value()) {\n    entries =\n        *cista::deserialize<shape_cache_bucket, cista::mode::CAST>(*value);\n  }\n\n  if (auto const it = std::find_if(begin(entries), end(entries),\n                                   [&](shape_cache_payload const& payload) {\n                                     return payload.key_ == key;\n                                   });\n      it != end(entries)) {\n    it->entry_ = entry;\n  } else {\n    entries.emplace_back(shape_cache_payload{.key_ = key, .entry_ = entry});\n  }\n\n  auto const serialized = cista::serialize(entries);\n  txn.put(dbi, bucket_key, to_string_view(serialized));\n  txn.commit();\n\n  auto const now = std::chrono::steady_clock::now();\n  if (now - last_sync_ >= std::chrono::minutes{1}) {\n    sync();\n    last_sync_ = now;\n  }\n}\n\nvoid shape_cache::sync() { env_.force_sync(); }\n\nstd::optional<osr::search_profile> get_profile(n::clasz const clasz) {\n  switch (clasz) {\n    case n::clasz::kBus:\n    case n::clasz::kCoach:\n    case n::clasz::kRideSharing:\n    case n::clasz::kODM: return osr::search_profile::kBus;\n    case n::clasz::kTram:\n    case n::clasz::kHighSpeed:\n    case n::clasz::kLongDistance:\n    case n::clasz::kNight:\n    case n::clasz::kRegional:\n    case n::clasz::kSuburban:\n    case n::clasz::kSubway:\n    case n::clasz::kFunicular: return osr::search_profile::kRailway;\n    case n::clasz::kShip: return osr::search_profile::kFerry;\n    default: return std::nullopt;\n  }\n}\n\nstruct route_shape_result {\n  n::vector<geo::latlng> shape_;\n  n::vector<n::shape_offset_t> offsets_;\n  geo::box route_bbox_{};\n  n::vector<geo::box> segment_bboxes_;\n\n  unsigned segments_routed_{};\n  unsigned segments_beelined_{};\n  unsigned dijkstra_early_terminations_{};\n  unsigned dijkstra_full_runs_{};\n};\n\nroute_shape_result route_shape(\n    osr::ways const& w,\n    osr::lookup const& lookup,\n    n::timetable const& tt,\n    std::vector<osr::location> const& match_points,\n    osr::search_profile const profile,\n    osr::profile_parameters const& profile_params,\n    n::clasz const clasz,\n    n::route_idx_t const route_idx,\n    std::optional<config::timetable::shapes_debug> const& debug,\n    bool const debug_enabled) {\n  auto r = route_shape_result{};\n  r.offsets_.reserve(\n      static_cast<decltype(r.offsets_)::size_type>(match_points.size()));\n  r.segment_bboxes_.reserve(static_cast<decltype(r.segment_bboxes_)::size_type>(\n      match_points.size() - 1U));\n\n  auto debug_fn =\n      std::function<void(osr::matched_route const&,\n                         std::function<boost::json::object()>)>{nullptr};\n\n  if (debug_enabled) {\n    debug_fn = [&debug, route_idx, clasz, &tt](\n                   osr::matched_route const& res,\n                   std::function<boost::json::object()> const& get_debug_json) {\n      auto include =\n          debug->all_ || (debug->all_with_beelines_ && res.n_beelined_ > 0U);\n      auto tags = std::set<std::string>{};\n\n      if (debug->route_indices_ && !debug->route_indices_->empty()) {\n        auto const& debug_route_indices = *debug->route_indices_;\n        if (std::ranges::contains(debug_route_indices, to_idx(route_idx))) {\n          include = true;\n        }\n      }\n\n      if (debug->route_ids_ && !debug->route_ids_->empty()) {\n        auto const& debug_route_ids = *debug->route_ids_;\n        for (auto const transport_idx : tt.route_transport_ranges_[route_idx]) {\n          auto const frun = n::rt::frun{\n              tt, nullptr,\n              n::rt::run{.t_ = n::transport{transport_idx, n::day_idx_t{0}},\n                         .stop_range_ =\n                             n::interval{\n                                 n::stop_idx_t{0U},\n                                 static_cast<n::stop_idx_t>(\n                                     tt.route_location_seq_[route_idx].size())},\n                         .rt_ = n::rt_transport_idx_t::invalid()}};\n\n          auto const rsn = frun[0].get_route_id(n::event_type::kDep);\n          if (std::ranges::contains(debug_route_ids, rsn)) {\n            tags.emplace(fmt::format(\"route_{}\", rsn));\n            include = true;\n            break;\n          }\n        }\n      }\n\n      if (debug->trips_ && !debug->trips_->empty()) {\n        auto const& debug_trip_ids = *debug->trips_;\n        for (auto const transport_idx : tt.route_transport_ranges_[route_idx]) {\n          auto const frun = n::rt::frun{\n              tt, nullptr,\n              n::rt::run{.t_ = n::transport{transport_idx},\n                         .stop_range_ =\n                             n::interval{\n                                 n::stop_idx_t{0U},\n                                 static_cast<n::stop_idx_t>(\n                                     tt.route_location_seq_[route_idx].size())},\n                         .rt_ = n::rt_transport_idx_t::invalid()}};\n\n          frun.for_each_trip([&](n::trip_idx_t const trip_idx,\n                                 n::interval<n::stop_idx_t> const) {\n            for (auto const trip_id_idx : tt.trip_ids_[trip_idx]) {\n              auto const trip_id = tt.trip_id_strings_.at(trip_id_idx).view();\n              if (std::ranges::contains(debug_trip_ids, trip_id)) {\n                tags.emplace(fmt::format(\"trip_{}\", trip_id));\n                include = true;\n                return;\n              }\n            }\n          });\n        }\n      }\n\n      auto const total_duration_ms = res.total_duration_.count();\n      if (debug->slow_ != 0U && total_duration_ms > debug->slow_) {\n        include = true;\n        tags.emplace(\"slow\");\n      }\n\n      if (include) {\n        auto fn = fmt::format(\"r_{}_{}\", to_idx(route_idx), to_str(clasz));\n        for (auto const& tag : tags) {\n          fn += fmt::format(\"_{}\", tag);\n        }\n        auto out_path = debug->path_ / fmt::format(\"{}.json.gz\", fn);\n        osr::write_map_match_debug(get_debug_json(), out_path);\n      }\n    };\n  }\n\n  auto const matched_route =\n      osr::map_match(w, lookup, profile, profile_params, match_points, nullptr,\n                     nullptr, debug_fn);\n\n  r.segments_routed_ = matched_route.n_routed_;\n  r.segments_beelined_ = matched_route.n_beelined_;\n  r.dijkstra_early_terminations_ = matched_route.n_dijkstra_early_terminations_;\n  r.dijkstra_full_runs_ = matched_route.n_dijkstra_full_runs_;\n\n  utl::verify(matched_route.segment_offsets_.size() == match_points.size(),\n              \"[route_shapes] segment offsets ({}) != match points ({})\",\n              matched_route.segment_offsets_.size(), match_points.size());\n\n  r.segment_bboxes_.resize(static_cast<decltype(r.segment_bboxes_)::size_type>(\n      match_points.size() - 1U));\n  r.shape_.clear();\n  r.shape_.reserve(static_cast<decltype(r.shape_)::size_type>(\n      matched_route.path_.segments_.size() * 8U));\n  r.offsets_.clear();\n  r.offsets_.reserve(\n      static_cast<decltype(r.offsets_)::size_type>(match_points.size()));\n  r.offsets_.emplace_back(0U);\n\n  for (auto seg_idx = 0U; seg_idx < match_points.size() - 1U; ++seg_idx) {\n    auto& seg_bbox = r.segment_bboxes_[seg_idx];\n\n    if (!r.shape_.empty()) {\n      seg_bbox.extend(r.shape_.back());\n    }\n\n    auto const start = matched_route.segment_offsets_[seg_idx];\n    auto const end = (seg_idx + 1U < match_points.size() - 1U)\n                         ? matched_route.segment_offsets_[seg_idx + 1U]\n                         : matched_route.path_.segments_.size();\n\n    for (auto ps_idx = start; ps_idx < end; ++ps_idx) {\n      auto const& ps = matched_route.path_.segments_[ps_idx];\n      for (auto const& pt : ps.polyline_) {\n        r.route_bbox_.extend(pt);\n        seg_bbox.extend(pt);\n      }\n      if (!ps.polyline_.empty()) {\n        auto first = ps.polyline_.begin();\n        if (!r.shape_.empty() && r.shape_.back() == *first) {\n          ++first;\n        }\n        r.shape_.insert(r.shape_.end(), first, ps.polyline_.end());\n      }\n    }\n\n    r.offsets_.emplace_back(static_cast<std::uint32_t>(r.shape_.size() - 1U));\n  }\n\n  utl::verify(r.offsets_.size() == match_points.size(),\n              \"[route_shapes] mismatch: offsets.size()={}, stops.size()={}\",\n              r.offsets_.size(), match_points.size());\n  return r;\n}\n\nboost::json::object route_shape_debug(osr::ways const& w,\n                                      osr::lookup const& lookup,\n                                      n::timetable const& tt,\n                                      n::route_idx_t const route_idx) {\n  utl::verify(route_idx < tt.n_routes(), \"invalid route index {}\", route_idx);\n\n  auto const clasz = tt.route_clasz_[route_idx];\n  auto const profile = get_profile(clasz);\n  utl::verify(profile.has_value(), \"route {} has unsupported class {}\",\n              route_idx, to_str(clasz));\n\n  auto const match_points =\n      utl::to_vec(tt.route_location_seq_[route_idx], [&](auto const stop_idx) {\n        auto const loc_idx = n::stop{stop_idx}.location_idx();\n        auto const pos = tt.locations_.coordinates_[loc_idx];\n        return osr::location{pos, osr::kNoLevel};\n      });\n\n  auto debug_json = boost::json::object{};\n  auto const profile_params = osr::get_parameters(*profile);\n  static_cast<void>(osr::map_match(\n      w, lookup, *profile, profile_params, match_points, nullptr, nullptr,\n      [&](osr::matched_route const&,\n          std::function<boost::json::object()> const& get_debug_json) {\n        debug_json = get_debug_json();\n      }));\n\n  return debug_json;\n}\n\nvoid route_shapes(osr::ways const& w,\n                  osr::lookup const& lookup,\n                  n::timetable const& tt,\n                  n::shapes_storage& shapes,\n                  config::timetable::route_shapes const& conf,\n                  std::array<bool, n::kNumClasses> const& clasz_enabled,\n                  shape_cache* cache) {\n  fmt::println(std::clog, \"computing shapes\");\n\n  auto const progress_tracker = utl::get_active_progress_tracker();\n  progress_tracker->status(\"Computing shapes\")\n      .out_bounds(0.F, 100.F)\n      .in_high(tt.n_routes());\n\n  auto routes_matched = 0ULL;\n  auto segments_routed = 0ULL;\n  auto segments_beelined = 0ULL;\n  auto dijkstra_early_terminations = 0ULL;\n  auto dijkstra_full_runs = 0ULL;\n  auto routes_with_existing_shapes = 0ULL;\n  auto cache_hits = 0ULL;\n\n  auto const& debug = conf.debug_;\n  auto const debug_enabled =\n      debug && !debug->path_.empty() &&\n      (debug->all_ || debug->all_with_beelines_ ||\n       (debug->trips_ && !debug->trips_->empty()) ||\n       (debug->route_ids_ && !debug->route_ids_->empty()) ||\n       (debug->route_indices_ && !debug->route_indices_->empty()) ||\n       debug->slow_ != 0U);\n\n  if (debug_enabled) {\n    std::filesystem::create_directories(debug->path_);\n  }\n\n  std::clog << \"\\n** route_shapes [start] **\\n\"\n            << \"  routes=\" << tt.n_routes() << \"\\n  trips=\" << tt.n_trips()\n            << \"\\n  shapes.trip_offset_indices_=\"\n            << shapes.trip_offset_indices_.size()\n            << \"\\n  shapes.route_bboxes_=\" << shapes.route_bboxes_.size()\n            << \"\\n  shapes.route_segment_bboxes_=\"\n            << shapes.route_segment_bboxes_.size()\n            << \"\\n  shapes.data=\" << shapes.data_.size()\n            << \"\\n  shapes.routed_data=\" << shapes.routed_data_.size()\n            << \"\\n  shapes.offsets=\" << shapes.offsets_.size()\n            << \"\\n  shapes.trip_offset_indices_=\"\n            << shapes.trip_offset_indices_.size() << \"\\n\\n\";\n\n  shapes.trip_offset_indices_.resize(tt.n_trips());\n  shapes.route_bboxes_.resize(tt.n_routes());\n  shapes.route_segment_bboxes_.resize(tt.n_routes());\n\n  auto shapes_mutex = std::mutex{};\n\n  auto const store_shape =\n      [&](n::route_idx_t const r, n::scoped_shape_idx_t const shape_idx,\n          auto const& offsets, geo::box const& route_bbox,\n          auto const& segment_bboxes,\n          std::vector<osr::location> const& match_points,\n          n::interval<n::transport_idx_t> const& transports) {\n        shapes.route_bboxes_[r] = route_bbox;\n        auto rsb = shapes.route_segment_bboxes_[r];\n        if (!rsb.empty()) {\n          if (rsb.size() != segment_bboxes.size()) {\n            fmt::println(std::clog,\n                         \"[route_shapes] route {}: segment bbox size \"\n                         \"mismatch: storage={}, computed={}\",\n                         r, rsb.size(), segment_bboxes.size());\n          } else {\n            for (auto i = 0U; i < segment_bboxes.size(); ++i) {\n              rsb[i] = segment_bboxes[i];\n            }\n          }\n        }\n\n        auto range_to_offsets =\n            hash_map<std::pair<n::stop_idx_t, n::stop_idx_t>,\n                     n::shape_offset_idx_t>{};\n\n        for (auto const transport_idx : transports) {\n          auto const frun = n::rt::frun{\n              tt, nullptr,\n              n::rt::run{.t_ = n::transport{transport_idx, n::day_idx_t{0}},\n                         .stop_range_ = n::interval{n::stop_idx_t{0U},\n                                                    static_cast<n::stop_idx_t>(\n                                                        match_points.size())},\n                         .rt_ = n::rt_transport_idx_t::invalid()}};\n          frun.for_each_trip([&](n::trip_idx_t const trip_idx,\n                                 n::interval<n::stop_idx_t> const range) {\n            auto const key = std::pair{range.from_, range.to_};\n            auto it = range_to_offsets.find(key);\n            if (it == end(range_to_offsets)) {\n              auto trip_offsets = std::vector<n::shape_offset_t>{};\n              trip_offsets.reserve(static_cast<std::size_t>(range.size()));\n              for (auto const i : range) {\n                trip_offsets.push_back(offsets.at(i));\n              }\n              auto const offsets_idx = shapes.add_offsets(trip_offsets);\n              it = range_to_offsets.emplace(key, offsets_idx).first;\n            }\n\n            shapes.trip_offset_indices_[trip_idx] = {shape_idx, it->second};\n          });\n        }\n      };\n\n  auto const process_route = [&](std::size_t const route_idx) {\n    auto const r =\n        n::route_idx_t{static_cast<n::route_idx_t::value_t>(route_idx)};\n\n    auto const clasz = tt.route_clasz_[r];\n    auto profile = get_profile(clasz);\n    if (!profile || !clasz_enabled[static_cast<std::size_t>(clasz)]) {\n      progress_tracker->increment();\n      return;\n    }\n    auto const profile_params = osr::get_parameters(*profile);\n\n    auto const stops = tt.route_location_seq_[r];\n    if (stops.size() < 2U ||\n        (conf.max_stops_ != 0U && stops.size() > conf.max_stops_)) {\n      auto l = std::scoped_lock{shapes_mutex};\n      std::clog << \"skipping route \" << r << \", \" << stops.size() << \" stops\\n\";\n      progress_tracker->increment();\n      return;\n    }\n\n    auto const transports = tt.route_transport_ranges_[r];\n\n    if (conf.mode_ == config::timetable::route_shapes::mode::missing) {\n      auto existing_shapes = true;\n      for (auto const transport_idx : transports) {\n        auto const frun = n::rt::frun{\n            tt, nullptr,\n            n::rt::run{.t_ = n::transport{transport_idx, n::day_idx_t{0}},\n                       .stop_range_ = n::interval{n::stop_idx_t{0U},\n                                                  static_cast<n::stop_idx_t>(\n                                                      stops.size())},\n                       .rt_ = n::rt_transport_idx_t::invalid()}};\n        frun.for_each_trip(\n            [&](n::trip_idx_t const trip_idx, n::interval<n::stop_idx_t>) {\n              auto const shape_idx = shapes.get_shape_idx(trip_idx);\n              if (shape_idx == n::scoped_shape_idx_t::invalid()) {\n                existing_shapes = false;\n              }\n            });\n        if (existing_shapes) {\n          ++routes_with_existing_shapes;\n          progress_tracker->increment();\n          return;\n        }\n      }\n    }\n\n    auto const match_points = utl::to_vec(stops, [&](auto const stop_idx) {\n      auto const loc_idx = n::stop{stop_idx}.location_idx();\n      auto const pos = tt.locations_.coordinates_[loc_idx];\n      return osr::location{pos, osr::level_t{osr::kNoLevel}};\n    });\n\n    try {\n      auto cache_key =\n          cache != nullptr\n              ? std::optional{shape_cache_key{\n                    *profile,\n                    cista::offset::to_vec(\n                        match_points, [](auto const& mp) { return mp.pos_; })}}\n              : std::nullopt;\n\n      if (cache != nullptr) {\n        if (auto const ce = cache->get(*cache_key); ce.has_value()) {\n          ++cache_hits;\n          auto const local_shape_idx = n::get_local_shape_idx(ce->shape_idx_);\n          utl::verify(ce->shape_idx_ != n::scoped_shape_idx_t::invalid() &&\n                          n::get_shape_source(ce->shape_idx_) ==\n                              n::shape_source::kRouted,\n                      \"[route_shapes] invalid cached shape index: {}\",\n                      ce->shape_idx_);\n          utl::verify(\n              local_shape_idx != n::shape_idx_t::invalid() &&\n                  static_cast<std::size_t>(to_idx(local_shape_idx)) <\n                      shapes.routed_data_.size(),\n              \"[route_shapes] cache routed shape idx out of bounds: {} >= {}\",\n              local_shape_idx, shapes.routed_data_.size());\n          auto const l = std::scoped_lock{shapes_mutex};\n          store_shape(r, ce->shape_idx_, ce->offsets_, ce->route_bbox_,\n                      ce->segment_bboxes_, match_points, transports);\n          progress_tracker->increment();\n          return;\n        }\n      }\n\n      auto rsr = route_shape(w, lookup, tt, match_points, *profile,\n                             profile_params, clasz, r, debug, debug_enabled);\n      ++routes_matched;\n      segments_routed += rsr.segments_routed_;\n      segments_beelined += rsr.segments_beelined_;\n      dijkstra_early_terminations += rsr.dijkstra_early_terminations_;\n      dijkstra_full_runs += rsr.dijkstra_full_runs_;\n\n      auto const l = std::scoped_lock{shapes_mutex};\n\n      auto const local_shape_idx =\n          static_cast<n::shape_idx_t>(shapes.routed_data_.size());\n      auto const shape_idx =\n          n::to_scoped_shape_idx(local_shape_idx, n::shape_source::kRouted);\n      shapes.routed_data_.emplace_back(rsr.shape_);\n\n      store_shape(r, shape_idx, rsr.offsets_, rsr.route_bbox_,\n                  rsr.segment_bboxes_, match_points, transports);\n      if (cache != nullptr) {\n        cache->put(\n            *cache_key,\n            shape_cache_entry{\n                .shape_idx_ = shape_idx,\n                .offsets_ = cista::offset::to_vec(rsr.offsets_),\n                .route_bbox_ = rsr.route_bbox_,\n                .segment_bboxes_ = cista::offset::to_vec(rsr.segment_bboxes_)});\n      }\n    } catch (std::exception const& e) {\n      fmt::println(std::clog,\n                   \"[route_shapes] route {}: map matching failed: {}\", r,\n                   e.what());\n\n      if (auto const trace =\n              boost::stacktrace::stacktrace::from_current_exception();\n          trace) {\n        std::clog << trace << std::endl;\n      }\n    }\n    progress_tracker->increment();\n  };\n\n  utl::parallel_for_run(\n      tt.n_routes(), process_route, utl::noop_progress_update{},\n      utl::parallel_error_strategy::QUIT_EXEC, conf.n_threads_);\n\n  if (cache != nullptr) {\n    cache->sync();\n  }\n\n  std::clog << \"\\n** route_shapes [end] **\\n\"\n            << \"  routes=\" << tt.n_routes() << \"\\n  trips=\" << tt.n_trips()\n            << \"\\n  shapes.trip_offset_indices_=\"\n            << shapes.trip_offset_indices_.size()\n            << \"\\n  shapes.route_bboxes_=\" << shapes.route_bboxes_.size()\n            << \"\\n  shapes.route_segment_bboxes_=\"\n            << shapes.route_segment_bboxes_.size()\n            << \"\\n  shapes.data=\" << shapes.data_.size()\n            << \"\\n  shapes.routed_data=\" << shapes.routed_data_.size()\n            << \"\\n  shapes.offsets=\" << shapes.offsets_.size()\n            << \"\\n  shapes.trip_offset_indices_=\"\n            << shapes.trip_offset_indices_.size() << \"\\n\\n\";\n\n  fmt::println(std::clog,\n               \"{} routes matched, {} segments routed, {} segments beelined, \"\n               \"{} dijkstra early terminations, {} dijkstra full runs\\n{} \"\n               \"routes with existing shapes skipped\\n{} cache hits\",\n               routes_matched, segments_routed, segments_beelined,\n               dijkstra_early_terminations, dijkstra_full_runs,\n               routes_with_existing_shapes, cache_hits);\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/rt/auser.cc",
    "content": "#include \"motis/rt/auser.h\"\n\n#include \"pugixml.hpp\"\n\n#include \"nigiri/common/parse_time.h\"\n#include \"nigiri/rt/json_to_xml.h\"\n\n#include \"motis/http_req.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nauser::auser(nigiri::timetable const& tt,\n             n::source_idx_t const s,\n             n::rt::vdv_aus::updater::xml_format const format)\n    : upd_{tt, s, format} {}\n\nstd::string auser::fetch_url(std::string_view base_url) {\n  return upd_.get_format() == n::rt::vdv_aus::updater::xml_format::kVdv\n             ? fmt::format(\"{}/auser/fetch?since={}&body_limit={}\", base_url,\n                           update_state_, kBodySizeLimit)\n             : std::string{base_url};\n}\n\nn::rt::vdv_aus::statistics auser::consume_update(\n    std::string const& auser_update, n::rt_timetable& rtt, bool const inplace) {\n  auto vdvaus = pugi::xml_document{};\n  if (upd_.get_format() == n::rt::vdv_aus::updater::xml_format::kSiriJson) {\n    vdvaus = n::rt::to_xml(auser_update);\n  } else {\n    inplace ? vdvaus.load_buffer_inplace(\n                  const_cast<void*>(\n                      reinterpret_cast<void const*>(auser_update.data())),\n                  auser_update.size())\n            : vdvaus.load_string(auser_update.c_str());\n  }\n\n  auto stats = upd_.update(rtt, vdvaus);\n\n  try {\n    auto const prev_update = update_state_;\n    update_state_ =\n        upd_.get_format() == n::rt::vdv_aus::updater::xml_format::kVdv\n            ? [&]() {\n              auto const opt1 = vdvaus.child(\"DatenAbrufenAntwort\")\n                 .child(\"AUSNachricht\")\n                 .attribute(\"auser_id\")\n                 .as_llong(0ULL);\n              auto const opt2 = vdvaus.child(\"AUSNachricht\")\n                 .attribute(\"auser_id\")\n                 .as_llong(0ULL);\n              return opt1 ? opt1 : opt2;\n            }()\n            : n::parse_time_no_tz(vdvaus.child(\"Siri\")\n                                      .child(\"ServiceDelivery\")\n                                      .child_value(\"ResponseTimestamp\"))\n                  .time_since_epoch()\n                  .count();\n    fmt::println(\"[auser] {} --> {}\", prev_update, update_state_);\n  } catch (...) {\n  }\n\n  return stats;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/rt_update.cc",
    "content": "#include \"motis/rt_update.h\"\n\n#include <filesystem>\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/asio/experimental/parallel_group.hpp\"\n#include \"boost/asio/redirect_error.hpp\"\n#include \"boost/asio/steady_timer.hpp\"\n#include \"boost/beast/core/buffers_to_string.hpp\"\n\n#include \"utl/read_file.h\"\n#include \"utl/timer.h\"\n\n#include \"nigiri/rt/create_rt_timetable.h\"\n#include \"nigiri/rt/gtfsrt_update.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/elevators/update_elevators.h\"\n#include \"motis/http_req.h\"\n#include \"motis/railviz.h\"\n#include \"motis/rt/auser.h\"\n#include \"motis/rt/rt_metrics.h\"\n#include \"motis/tag_lookup.h\"\n\nnamespace n = nigiri;\nnamespace asio = boost::asio;\nnamespace fs = std::filesystem;\nusing asio::awaitable;\n\nnamespace motis {\n\nasio::awaitable<ptr<elevators>> update_elevators(config const& c,\n                                                 data const& d,\n                                                 n::rt_timetable& new_rtt) {\n  utl::verify(c.has_elevators() && c.get_elevators()->url_ && c.timetable_,\n              \"elevator update requires settings for timetable + elevators\");\n  auto const res =\n      co_await http_GET(boost::urls::url{*c.get_elevators()->url_},\n                        c.get_elevators()->headers_.value_or(headers_t{}),\n                        std::chrono::seconds{c.get_elevators()->http_timeout_});\n  co_return update_elevators(c, d, get_http_body(res), new_rtt);\n}\n\nstd::string get_dump_path(auto&& ep) {\n  auto const normalize = [](std::string const& x) {\n    auto ret = std::string{};\n    ret.resize(x.size());\n    for (auto [to, from] : utl::zip(ret, x)) {\n      auto const c = from;\n      if (('0' <= c && c <= '9') ||  //\n          ('a' <= c && c <= 'z') ||  //\n          ('A' <= c && c <= 'Z')) {\n        to = c;\n      } else {\n        to = '_';\n      }\n    }\n    return ret;\n  };\n  return fmt::format(\"dump_rt/{}-{}\", ep.tag_, normalize(ep.ep_.url_));\n}\n\nstruct gtfs_rt_endpoint {\n  config::timetable::dataset::rt ep_;\n  n::source_idx_t src_;\n  std::string tag_;\n  gtfsrt_metrics metrics_;\n};\n\nstruct auser_endpoint {\n  config::timetable::dataset::rt ep_;\n  n::source_idx_t src_;\n  std::string tag_;\n  vdvaus_metrics metrics_;\n};\n\nvoid run_rt_update(boost::asio::io_context& ioc, config const& c, data& d) {\n  boost::asio::co_spawn(\n      ioc,\n      [&c, &d]() -> awaitable<void> {\n        auto const dump_rt = fs::is_directory(\"dump_rt\");\n        if (dump_rt) {\n          fmt::println(\"WARNING: DUMPING TO dump_rt\\n\");\n        }\n\n        auto executor = co_await asio::this_coro::executor;\n        auto timer = asio::steady_timer{executor};\n        auto ec = boost::system::error_code{};\n\n        auto const endpoints = [&]() {\n          auto endpoints =\n              std::vector<std::variant<gtfs_rt_endpoint, auser_endpoint>>{};\n          auto const metric_families =\n              rt_metric_families{d.metrics_->registry_};\n          for (auto const& [tag, dataset] : c.timetable_->datasets_) {\n            if (dataset.rt_.has_value()) {\n              auto const src = d.tags_->get_src(tag);\n              for (auto const& ep : *dataset.rt_) {\n                switch (ep.protocol_) {\n                  case config::timetable::dataset::rt::protocol::gtfsrt:\n                    endpoints.push_back(gtfs_rt_endpoint{\n                        ep, src, tag, gtfsrt_metrics{tag, metric_families}});\n                    break;\n                  case config::timetable::dataset::rt::protocol::siri_json:\n                  case config::timetable::dataset::rt::protocol::siri:\n                    [[fallthrough]];\n                  case config::timetable::dataset::rt::protocol::auser:\n                    endpoints.push_back(auser_endpoint{\n                        ep, src, tag, vdvaus_metrics{tag, metric_families}});\n                    break;\n                }\n              }\n            }\n          }\n          return endpoints;\n        }();\n\n        while (true) {\n          // Remember when we started, so we can schedule the next update.\n          auto const start = std::chrono::steady_clock::now();\n\n          {\n            auto t = utl::scoped_timer{\"rt update\"};\n\n            // Create new real-time timetable.\n            auto const today = std::chrono::time_point_cast<date::days>(\n                std::chrono::system_clock::now());\n            auto rtt = std::make_unique<n::rt_timetable>(\n                c.timetable_->incremental_rt_update_\n                    ? n::rt_timetable{*d.rt_->rtt_}\n                    : n::rt::create_rt_timetable(*d.tt_, today));\n\n            // Schedule updates for each real-time endpoint.\n            auto const timeout =\n                std::chrono::seconds{c.timetable_->http_timeout_};\n\n            using stats_t =\n                std::variant<n::rt::statistics, n::rt::vdv_aus::statistics>;\n            if (c.timetable_->canned_rt_) {\n              fmt::println(\"WARNING: READING CANNED RT\");\n\n              auto const stats =\n                  utl::to_vec(endpoints, [&](auto&& ep) -> stats_t {\n                    try {\n                      return utl::visit(\n                          ep,\n                          [&](gtfs_rt_endpoint const& g) -> stats_t {\n                            auto const path = get_dump_path(g);\n                            auto const body = utl::read_file(path.c_str());\n                            if (body.has_value()) {\n                              return n::rt::gtfsrt_update_buf(\n                                  *d.tt_, *rtt, g.src_, g.tag_, *body);\n                            } else {\n                              return n::rt::statistics{.parser_error_ = true};\n                            }\n                          },\n                          [&](auser_endpoint const& a) -> stats_t {\n                            auto const path = get_dump_path(a);\n                            auto& auser = d.auser_->at(a.ep_.url_);\n                            auto const body = utl::read_file(path.c_str());\n                            if (body.has_value()) {\n                              return auser.consume_update(*body, *rtt);\n                            } else {\n                              return n::rt::vdv_aus::statistics{.error_ = true};\n                            }\n                          });\n                    } catch (std::exception const& e) {\n                      std::cout << \"EXCEPTION: \" << e.what() << \"\\n\";\n                      return n::rt::statistics{.parser_error_ = true};\n                    }\n                  });\n\n              for (auto const [s, ep] : utl::zip(stats, endpoints)) {\n                utl::visit(\n                    ep,\n                    [&](gtfs_rt_endpoint const& g) {\n                      n::log(n::log_lvl::info, \"motis.rt\",\n                             \"GTFS-RT update stats for tag={}, url={}: {}\",\n                             g.tag_, g.ep_.url_,\n                             fmt::streamed(std::get<n::rt::statistics>(s)));\n                    },\n                    [&](auser_endpoint const& a) {\n                      n::log(n::log_lvl::info, \"motis.rt\",\n                             \"VDV AUS update stats for tag={}, url={}:\\n{}\",\n                             a.tag_, a.ep_.url_,\n                             fmt::streamed(\n                                 std::get<n::rt::vdv_aus::statistics>(s)));\n                    });\n              }\n            } else if (!endpoints.empty()) {\n              auto awaitables = utl::to_vec(\n                  endpoints,\n                  [&](std::variant<gtfs_rt_endpoint, auser_endpoint> const& x) {\n                    return boost::asio::co_spawn(\n                        executor,\n                        [&]() -> awaitable<\n                                  std::variant<n::rt::statistics,\n                                               n::rt::vdv_aus::statistics>> {\n                          auto ret = std::variant<n::rt::statistics,\n                                                  n::rt::vdv_aus::statistics>{};\n                          co_await std::visit(\n                              utl::overloaded{\n                                  [&](gtfs_rt_endpoint const& g)\n                                      -> awaitable<void> {\n                                    g.metrics_.updates_requested_.Increment();\n                                    try {\n                                      auto const res = co_await http_GET(\n                                          boost::urls::url{g.ep_.url_},\n                                          g.ep_.headers_.value_or(headers_t{}),\n                                          timeout);\n                                      auto const body = get_http_body(res);\n                                      if (dump_rt) {\n                                        std::ofstream{get_dump_path(g)}.write(\n                                            body.c_str(),\n                                            static_cast<long>(body.size()));\n                                      }\n                                      ret = n::rt::gtfsrt_update_buf(\n                                          *d.tt_, *rtt, g.src_, g.tag_, body);\n                                    } catch (std::exception const& e) {\n                                      g.metrics_.updates_error_.Increment();\n                                      n::log(n::log_lvl::error, \"motis.rt\",\n                                             \"RT FETCH ERROR: tag={}, error={}\",\n                                             g.tag_, e.what());\n                                      ret = n::rt::statistics{\n                                          .parser_error_ = true,\n                                          .no_header_ = true};\n                                    }\n                                  },\n                                  [&](auser_endpoint const& a)\n                                      -> awaitable<void> {\n                                    a.metrics_.updates_requested_.Increment();\n                                    auto& auser = d.auser_->at(a.ep_.url_);\n                                    try {\n                                      auto const fetch_url = boost::urls::url{\n                                          auser.fetch_url(a.ep_.url_)};\n                                      fmt::println(\"[auser] fetch url: {}\",\n                                                   fetch_url.c_str());\n                                      auto const res = co_await http_GET(\n                                          fetch_url,\n                                          a.ep_.headers_.value_or(headers_t{}),\n                                          timeout);\n                                      auto body = get_http_body(res);\n                                      if (dump_rt) {\n                                        std::ofstream{get_dump_path(a)}.write(\n                                            body.c_str(),\n                                            static_cast<long>(body.size()));\n                                      }\n                                      ret = auser.consume_update(body, *rtt,\n                                                                 true);\n                                    } catch (std::exception const& e) {\n                                      a.metrics_.updates_error_.Increment();\n                                      n::log(n::log_lvl::error, \"motis.rt\",\n                                             \"VDV AUS FETCH ERROR: tag={}, \"\n                                             \"url={}, error={}\",\n                                             a.tag_, a.ep_.url_, e.what());\n                                      ret = nigiri::rt::vdv_aus::statistics{\n                                          .error_ = true};\n                                    }\n                                  }},\n                              x);\n                          co_return ret;\n                        },\n                        asio::deferred);\n                  });\n\n              // Wait for all updates to finish\n              auto [_, exceptions, stats] =\n                  co_await asio::experimental::make_parallel_group(awaitables)\n                      .async_wait(asio::experimental::wait_for_all(),\n                                  asio::use_awaitable);\n\n              //  Print statistics.\n              for (auto const [ep, ex, s] :\n                   utl::zip(endpoints, exceptions, stats)) {\n                std::visit(\n                    utl::overloaded{\n                        [&](gtfs_rt_endpoint const& g) {\n                          try {\n                            if (ex) {\n                              std::rethrow_exception(ex);\n                            }\n\n                            g.metrics_.updates_successful_.Increment();\n                            g.metrics_.last_update_timestamp_\n                                .SetToCurrentTime();\n                            g.metrics_.update(std::get<n::rt::statistics>(s));\n\n                            n::log(\n                                n::log_lvl::info, \"motis.rt\",\n                                \"GTFS-RT update stats for tag={}, url={}: {}\",\n                                g.tag_, g.ep_.url_,\n                                fmt::streamed(std::get<n::rt::statistics>(s)));\n                          } catch (std::exception const& e) {\n                            g.metrics_.updates_error_.Increment();\n                            n::log(n::log_lvl::error, \"motis.rt\",\n                                   \"GTFS-RT update failed: tag={}, url={}, \"\n                                   \"error={}\",\n                                   g.tag_, g.ep_.url_, e.what());\n                          }\n                        },\n                        [&](auser_endpoint const& a) {\n                          try {\n                            if (ex) {\n                              std::rethrow_exception(ex);\n                            }\n\n                            a.metrics_.updates_successful_.Increment();\n                            a.metrics_.last_update_timestamp_\n                                .SetToCurrentTime();\n                            a.metrics_.update(\n                                std::get<n::rt::vdv_aus::statistics>(s));\n\n                            n::log(\n                                n::log_lvl::info, \"motis.rt\",\n                                \"VDV AUS update stats for tag={}, url={}:\\n{}\",\n                                a.tag_, a.ep_.url_,\n                                fmt::streamed(\n                                    std::get<n::rt::vdv_aus::statistics>(s)));\n                          } catch (std::exception const& e) {\n                            a.metrics_.updates_error_.Increment();\n                            n::log(n::log_lvl::error, \"motis.rt\",\n                                   \"VDV AUS update failed: tag={}, url={}, \"\n                                   \"error={}\",\n                                   a.tag_, a.ep_.url_, e.what());\n                          }\n                        }},\n                    ep);\n              }\n            }\n\n            // Update lbs.\n            rtt->update_lbs(*d.tt_);\n\n            // Update real-time timetable shared pointer.\n            auto railviz_rt = std::make_unique<railviz_rt_index>(*d.tt_, *rtt);\n            auto elevators = c.has_elevators() && c.get_elevators()->url_\n                                 ? co_await update_elevators(c, d, *rtt)\n                                 : std::move(d.rt_->e_);\n            auto new_rt = std::make_shared<rt>(\n                std::move(rtt), std::move(elevators), std::move(railviz_rt));\n            std::atomic_store(&d.rt_, std::move(new_rt));\n          }\n\n          // Schedule next update.\n          timer.expires_at(\n              start + std::chrono::seconds{c.timetable_->update_interval_});\n          co_await timer.async_wait(\n              asio::redirect_error(asio::use_awaitable, ec));\n          if (ec == asio::error::operation_aborted) {\n            co_return;\n          }\n        }\n      },\n      boost::asio::detached);\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/server.cc",
    "content": "#include <string_view>\n\n#include \"boost/asio/io_context.hpp\"\n\n#include \"fmt/format.h\"\n\n#include \"net/lb.h\"\n#include \"net/run.h\"\n#include \"net/stop_handler.h\"\n#include \"net/web_server/web_server.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/init_from.h\"\n#include \"utl/logging.h\"\n#include \"utl/parser/arg_parser.h\"\n\n#include \"ctx/ctx.h\"\n\n#include \"motis/config.h\"\n#include \"motis/ctx_data.h\"\n#include \"motis/ctx_exec.h\"\n#include \"motis/data.h\"\n#include \"motis/motis_instance.h\"\n\nnamespace fs = std::filesystem;\n\nnamespace motis {\n\nint server(data d, config const& c, std::string_view const motis_version) {\n  auto scheduler = ctx::scheduler<ctx_data>{};\n  auto m = motis_instance{ctx_exec{scheduler.runner_.ios(), scheduler}, d, c,\n                          motis_version};\n\n  auto lbs = std::vector<net::lb>{};\n  if (c.server_.value_or(config::server{}).lbs_) {\n    lbs = utl::to_vec(*c.server_.value_or(config::server{}).lbs_,\n                      [&](std::string const& url) {\n                        return net::lb{scheduler.runner_.ios(), url, m.qr_};\n                      });\n  }\n\n  auto s = net::web_server{scheduler.runner_.ios()};\n  s.set_timeout(std::chrono::minutes{5});\n  s.on_http_request(m.qr_);\n\n  auto ec = boost::system::error_code{};\n  auto const server_config = c.server_.value_or(config::server{});\n  s.init(server_config.host_, server_config.port_, ec);\n  if (ec) {\n    std::cerr << \"error: \" << ec << \"\\n\";\n    return 1;\n  }\n\n  auto const stop = net::stop_handler(scheduler.runner_.ios(), [&]() {\n    utl::log_info(\"motis.server\", \"shutdown\");\n    for (auto& lb : lbs) {\n      lb.stop();\n    }\n    s.stop();\n    m.stop();\n    scheduler.runner_.stop();\n  });\n\n  utl::log_info(\n      \"motis.server\",\n      \"n_threads={}, listening on {}:{}\\nlocal link: http://localhost:{}\",\n      c.n_threads(), server_config.host_, server_config.port_,\n      server_config.port_);\n\n  for (auto& lb : lbs) {\n    lb.run();\n  }\n  s.run();\n  m.run(d, c);\n  scheduler.runner_.run(c.n_threads());\n  m.join();\n\n  return 0;\n}\n\nunsigned get_api_version(boost::urls::url_view const& url) {\n  if (url.encoded_path().length() > 7) {\n    return utl::parse<unsigned>(\n        std::string_view{url.encoded_path().substr(6, 2)});\n  }\n  return 0U;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/tag_lookup.cc",
    "content": "#include \"motis/tag_lookup.h\"\n\n#include <ctime>\n\n#include \"fmt/chrono.h\"\n#include \"fmt/core.h\"\n\n#include \"cista/io.h\"\n\n#include \"utl/enumerate.h\"\n#include \"utl/parser/split.h\"\n#include \"utl/verify.h\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/not_found_exception.h\"\n\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/gtfsrt_resolve_run.h\"\n#include \"nigiri/timetable.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\ntrip_id<std::string_view> split_trip_id(std::string_view id) {\n  auto const [date, start_time, tag, trip_id] =\n      utl::split<'_', utl::cstr, utl::cstr, utl::cstr, utl::cstr>(id);\n\n  auto ret = motis::trip_id{};\n\n  utl::verify<net::bad_request_exception>(date.valid(),\n                                          \"invalid tripId date {}\", id);\n  ret.start_date_ = date.view();\n\n  utl::verify<net::bad_request_exception>(start_time.valid(),\n                                          \"invalid tripId start_time {}\", id);\n  ret.start_time_ = start_time.view();\n\n  utl::verify<net::bad_request_exception>(tag.valid(), \"invalid tripId tag {}\",\n                                          id);\n  ret.tag_ = tag.view();\n\n  // allow trip ids starting with underscore\n  auto const trip_id_len_plus_one =\n      static_cast<std::size_t>(id.data() + id.size() - tag.str) - tag.length();\n  utl::verify<net::bad_request_exception>(trip_id_len_plus_one > 1,\n                                          \"invalid tripId id {}\", id);\n  ret.trip_id_ =\n      std::string_view{tag.str + tag.length() + 1, trip_id_len_plus_one - 1};\n\n  return ret;\n}\n\nstd::pair<std::string_view, std::string_view> split_tag_id(std::string_view x) {\n  auto const first_underscore_pos = x.find('_');\n  return first_underscore_pos != std::string_view::npos\n             ? std::pair{x.substr(0, first_underscore_pos),\n                         x.substr(first_underscore_pos + 1U)}\n             : std::pair{std::string_view{}, x};\n}\n\nvoid tag_lookup::add(n::source_idx_t const src, std::string_view str) {\n  utl::verify<net::bad_request_exception>(tag_to_src_.size() == to_idx(src),\n                                          \"invalid tag\");\n  tag_to_src_.emplace(std::string{str}, src);\n  src_to_tag_.emplace_back(str);\n}\n\nn::source_idx_t tag_lookup::get_src(std::string_view tag) const {\n  auto const it = tag_to_src_.find(tag);\n  return it == end(tag_to_src_) ? n::source_idx_t::invalid() : it->second;\n}\n\nstd::string_view tag_lookup::get_tag(n::source_idx_t const src) const {\n  return src == n::source_idx_t::invalid() ? \"\" : src_to_tag_.at(src).view();\n}\n\nstd::string tag_lookup::id(n::timetable const& tt,\n                           n::location_idx_t const l) const {\n  auto const src = tt.locations_.src_.at(l);\n  auto const id = tt.locations_.ids_.at(l).view();\n  return src == n::source_idx_t::invalid()\n             ? std::string{id}\n             : fmt::format(\"{}_{}\", get_tag(src), id);\n}\n\ntrip_id<std::string> tag_lookup::id_fragments(\n    n::timetable const& tt,\n    n::rt::run_stop s,\n    n::event_type const ev_type) const {\n  if (s.fr_->is_scheduled()) {\n    // trip id\n    auto const t = s.get_trip_idx(ev_type);\n    auto const id_idx = tt.trip_ids_[t].front();\n    auto const id = tt.trip_id_strings_[id_idx].view();\n    auto const src = tt.trip_id_src_[id_idx];\n\n    // start date + start time\n    auto const [day, gtfs_static_dep] = s.get_trip_start(ev_type);\n    auto const start_hours = gtfs_static_dep / 60;\n    auto const start_minutes = gtfs_static_dep % 60;\n\n    return {\n        fmt::format(\"{:%Y%m%d}\", day),\n        fmt::format(\"{:02}:{:02}\", start_hours.count(), start_minutes.count()),\n        std::string{get_tag(src)}, std::string{id}};\n  } else {\n    auto const id = s.fr_->id();\n    auto const time = std::chrono::system_clock::to_time_t(\n        (*s.fr_)[0].time(n::event_type::kDep));\n    auto const utc = *std::gmtime(&time);\n    auto const id_tag = get_tag(id.src_);\n    auto const id_id = id.id_;\n    return {fmt::format(\"{:04}{:02}{:02}\", utc.tm_year + 1900, utc.tm_mon + 1,\n                        utc.tm_mday),\n            fmt::format(\"{:02}:{:02}\", utc.tm_hour, utc.tm_min),\n            std::string{id_tag}, std::string{id_id}};\n  }\n}\n\nstd::string tag_lookup::id(n::timetable const& tt,\n                           n::rt::run_stop s,\n                           n::event_type const ev_type) const {\n  auto const t = id_fragments(tt, s, ev_type);\n  return fmt::format(\"{}_{}_{}_{}\", std::move(t.start_date_),\n                     std::move(t.start_time_), std::move(t.tag_),\n                     std::move(t.trip_id_));\n}\n\nstd::string tag_lookup::route_id(n::rt::run_stop s,\n                                 n::event_type const ev_type) const {\n  return fmt::format(\"{}_{}\", get_tag(s.fr_->id().src_),\n                     s.get_route_id(ev_type));\n}\n\nstd::pair<n::rt::run, n::trip_idx_t> tag_lookup::get_trip(\n    n::timetable const& tt,\n    n::rt_timetable const* rtt,\n    std::string_view id) const {\n  auto const split = split_trip_id(id);\n  auto td = transit_realtime::TripDescriptor{};\n  td.set_start_date(split.start_date_);\n  td.set_start_time(split.start_time_);\n  td.set_trip_id(split.trip_id_);\n  return n::rt::gtfsrt_resolve_run({}, tt, rtt, get_src(split.tag_), td);\n}\n\nn::location_idx_t tag_lookup::get_location(n::timetable const& tt,\n                                           std::string_view s) const {\n  if (auto const res = find_location(tt, s); res.has_value()) {\n    return *res;\n  }\n  throw utl::fail<net::not_found_exception>(\n      \"Could not find timetable location {:?}\", s);\n}\n\nstd::optional<n::location_idx_t> tag_lookup::find_location(\n    n::timetable const& tt, std::string_view s) const {\n  auto const [tag, id] = split_tag_id(s);\n\n  auto const src = get_src(tag);\n  if (src == n::source_idx_t::invalid()) {\n    return std::nullopt;\n  }\n\n  auto const it = tt.locations_.location_id_to_idx_.find({id, src});\n  if (it == end(tt.locations_.location_id_to_idx_)) {\n    return std::nullopt;\n  }\n\n  return it->second;\n}\n\nvoid tag_lookup::write(std::filesystem::path const& p) const {\n  return cista::write(p, *this);\n}\n\ncista::wrapped<tag_lookup> tag_lookup::read(std::filesystem::path const& p) {\n  return cista::read<tag_lookup>(p);\n}\n\nstd::ostream& operator<<(std::ostream& out, tag_lookup const& tags) {\n  auto first = true;\n  for (auto const [src, tag] : utl::enumerate(tags.src_to_tag_)) {\n    if (!first) {\n      out << \", \";\n    }\n    first = false;\n    out << src << \"=\" << tag.view();\n  }\n  return out;\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/timetable/clasz_to_mode.cc",
    "content": "#include \"motis/timetable/clasz_to_mode.h\"\n\n#include \"utl/for_each_bit_set.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\napi::ModeEnum to_mode(n::clasz const c, unsigned const api_version) {\n  switch (c) {\n    case n::clasz::kAir: return api::ModeEnum::AIRPLANE;\n    case n::clasz::kHighSpeed: return api::ModeEnum::HIGHSPEED_RAIL;\n    case n::clasz::kLongDistance: return api::ModeEnum::LONG_DISTANCE;\n    case n::clasz::kCoach: return api::ModeEnum::COACH;\n    case n::clasz::kNight: return api::ModeEnum::NIGHT_RAIL;\n    case n::clasz::kRideSharing: return api::ModeEnum::RIDE_SHARING;\n    case n::clasz::kRegional: return api::ModeEnum::REGIONAL_RAIL;\n    case n::clasz::kSuburban:\n      return api_version < 5 ? api::ModeEnum::METRO : api::ModeEnum::SUBURBAN;\n    case n::clasz::kSubway: return api::ModeEnum::SUBWAY;\n    case n::clasz::kTram: return api::ModeEnum::TRAM;\n    case n::clasz::kBus: return api::ModeEnum::BUS;\n    case n::clasz::kShip: return api::ModeEnum::FERRY;\n    case n::clasz::kODM: return api::ModeEnum::ODM;\n    case n::clasz::kFunicular: return api::ModeEnum::FUNICULAR;\n    case n::clasz::kAerialLift:\n      return api_version < 5 ? api::ModeEnum::AREAL_LIFT\n                             : api::ModeEnum::AERIAL_LIFT;\n    case n::clasz::kOther: return api::ModeEnum::OTHER;\n    case n::clasz::kNumClasses:;\n  }\n  std::unreachable();\n}\n\nstd::vector<api::ModeEnum> to_modes(nigiri::routing::clasz_mask_t const mask,\n                                    unsigned api_version) {\n  auto modes = std::vector<api::ModeEnum>{};\n  utl::for_each_set_bit(mask, [&](auto const i) {\n    modes.emplace_back(to_mode(static_cast<n::clasz>(i), api_version));\n  });\n  return modes;\n}\n\n}  // namespace motis"
  },
  {
    "path": "src/timetable/modes_to_clasz_mask.cc",
    "content": "#include \"motis/timetable/modes_to_clasz_mask.h\"\n\nnamespace n = nigiri;\n\nnamespace motis {\n\nn::routing::clasz_mask_t to_clasz_mask(std::vector<api::ModeEnum> const& mode) {\n  auto mask = n::routing::clasz_mask_t{0U};\n  auto const allow = [&](n::clasz const c) {\n    mask |= (1U << static_cast<std::underlying_type_t<n::clasz>>(c));\n  };\n  for (auto const& m : mode) {\n    switch (m) {\n      case api::ModeEnum::TRANSIT:\n        mask = n::routing::all_clasz_allowed();\n        return mask;\n      case api::ModeEnum::TRAM: allow(n::clasz::kTram); break;\n      case api::ModeEnum::SUBWAY: allow(n::clasz::kSubway); break;\n      case api::ModeEnum::FERRY: allow(n::clasz::kShip); break;\n      case api::ModeEnum::AIRPLANE: allow(n::clasz::kAir); break;\n      case api::ModeEnum::BUS: allow(n::clasz::kBus); break;\n      case api::ModeEnum::COACH: allow(n::clasz::kCoach); break;\n      case api::ModeEnum::RAIL:\n        allow(n::clasz::kHighSpeed);\n        allow(n::clasz::kLongDistance);\n        allow(n::clasz::kNight);\n        allow(n::clasz::kRegional);\n        allow(n::clasz::kSuburban);\n        allow(n::clasz::kSubway);\n        break;\n      case api::ModeEnum::HIGHSPEED_RAIL: allow(n::clasz::kHighSpeed); break;\n      case api::ModeEnum::LONG_DISTANCE: allow(n::clasz::kLongDistance); break;\n      case api::ModeEnum::NIGHT_RAIL: allow(n::clasz::kNight); break;\n      case api::ModeEnum::RIDE_SHARING: allow(n::clasz::kRideSharing); break;\n      case api::ModeEnum::REGIONAL_FAST_RAIL: [[fallthrough]];\n      case api::ModeEnum::REGIONAL_RAIL: allow(n::clasz::kRegional); break;\n      case api::ModeEnum::SUBURBAN: allow(n::clasz::kSuburban); break;\n      case api::ModeEnum::METRO: allow(n::clasz::kSuburban); break;\n      case api::ModeEnum::ODM: allow(n::clasz::kODM); break;\n      case api::ModeEnum::CABLE_CAR: [[fallthrough]];\n      case api::ModeEnum::FUNICULAR: allow(n::clasz::kFunicular); break;\n      case api::ModeEnum::AERIAL_LIFT: allow(n::clasz::kAerialLift); break;\n      case api::ModeEnum::AREAL_LIFT: allow(n::clasz::kAerialLift); break;\n      case api::ModeEnum::OTHER: allow(n::clasz::kOther); break;\n\n      case api::ModeEnum::WALK:\n      case api::ModeEnum::BIKE:\n      case api::ModeEnum::RENTAL:\n      case api::ModeEnum::CAR:\n      case api::ModeEnum::FLEX:\n      case api::ModeEnum::DEBUG_BUS_ROUTE:\n      case api::ModeEnum::DEBUG_RAILWAY_ROUTE:\n      case api::ModeEnum::DEBUG_FERRY_ROUTE:\n      case api::ModeEnum::CAR_DROPOFF: [[fallthrough]];\n      case api::ModeEnum::CAR_PARKING: break;\n    }\n  }\n  return mask;\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "src/update_rtt_td_footpaths.cc",
    "content": "#include \"motis/update_rtt_td_footpaths.h\"\n\n#include <map>\n\n#include \"utl/equal_ranges_linear.h\"\n#include \"utl/parallel_for.h\"\n\n#include \"osr/routing/parameters.h\"\n#include \"osr/routing/route.h\"\n\n#include \"motis/constants.h\"\n#include \"motis/get_loc.h\"\n#include \"motis/get_stops_with_traffic.h\"\n#include \"motis/osr/max_distance.h\"\n\nnamespace n = nigiri;\nusing namespace std::chrono_literals;\n\nnamespace motis {\n\nusing node_states_t =\n    std::pair<nodes_t, std::vector<std::pair<n::unixtime_t, states_t>>>;\n\nnode_states_t get_node_states(osr::ways const& w,\n                              osr::lookup const& l,\n                              elevators const& e,\n                              geo::latlng const& pos) {\n  auto e_nodes =\n      utl::to_vec(l.find_elevators(geo::box{pos, kElevatorUpdateRadius}));\n  auto e_state_changes =\n      get_state_changes(\n          utl::to_vec(\n              e_nodes,\n              [&](osr::node_idx_t const n)\n                  -> std::vector<state_change<n::unixtime_t>> {\n                auto const ne =\n                    match_elevator(e.elevators_rtree_, e.elevators_, w, n);\n                if (ne == elevator_idx_t::invalid()) {\n                  return {\n                      {.valid_from_ = n::unixtime_t{n::unixtime_t::duration{0}},\n                       .state_ = true}};\n                }\n                return e.elevators_[ne].get_state_changes();\n              }))\n          .to_vec();\n  return {std::move(e_nodes), std::move(e_state_changes)};\n}\n\nosr::bitvec<osr::node_idx_t>& set_blocked(\n    nodes_t const& e_nodes,\n    states_t const& states,\n    osr::bitvec<osr::node_idx_t>& blocked_mem) {\n  blocked_mem.zero_out();\n  for (auto const [n, s] : utl::zip(e_nodes, states)) {\n    blocked_mem.set(n, !s);\n  }\n  return blocked_mem;\n}\n\nstd::optional<std::pair<nodes_t, states_t>> get_states_at(\n    osr::ways const& w,\n    osr::lookup const& l,\n    elevators const& e,\n    n::unixtime_t const t,\n    geo::latlng const& pos) {\n  auto const [e_nodes, e_state_changes] = get_node_states(w, l, e, pos);\n  if (e_nodes.empty()) {\n    return std::pair{nodes_t{}, states_t{}};\n  }\n  auto const it = std::lower_bound(\n      begin(e_state_changes), end(e_state_changes), t,\n      [&](auto&& a, n::unixtime_t const b) { return a.first < b; });\n  if (it == begin(e_state_changes)) {\n    return std::nullopt;\n  }\n  return std::pair{e_nodes, std::prev(it)->second};\n}\n\nstd::vector<n::td_footpath> get_td_footpaths(\n    osr::ways const& w,\n    osr::lookup const& l,\n    osr::platforms const& pl,\n    nigiri::timetable const& tt,\n    nigiri::rt_timetable const* rtt,\n    point_rtree<n::location_idx_t> const& loc_rtree,\n    elevators const& e,\n    platform_matches_t const& matches,\n    n::location_idx_t const start_l,\n    osr::location const start,\n    osr::direction const dir,\n    osr::search_profile const profile,\n    std::chrono::seconds const max,\n    double const max_matching_distance,\n    osr_parameters const& osr_params,\n    osr::bitvec<osr::node_idx_t>& blocked_mem) {\n  blocked_mem.resize(w.n_nodes());\n\n  auto const [e_nodes, e_state_changes] = get_node_states(w, l, e, start.pos_);\n\n  auto fps = std::vector<n::td_footpath>{};\n  for (auto const& [t, states] : e_state_changes) {\n    set_blocked(e_nodes, states, blocked_mem);\n\n    auto const neighbors = get_stops_with_traffic(\n        tt, rtt, loc_rtree, start, get_max_distance(profile, max), start_l);\n    auto const results = osr::route(\n        to_profile_parameters(profile, osr_params), w, l, profile, start,\n        utl::to_vec(neighbors,\n                    [&](auto&& x) { return get_loc(tt, w, pl, matches, x); }),\n        static_cast<osr::cost_t>(max.count()), dir, max_matching_distance,\n        &blocked_mem);\n\n    for (auto const [to, p] : utl::zip(neighbors, results)) {\n      auto const duration = p.has_value() && (n::duration_t{p->cost_ / 60U} <\n                                              n::footpath::kMaxDuration)\n                                ? n::duration_t{p->cost_ / 60U}\n                                : n::footpath::kMaxDuration;\n      fps.push_back(n::td_footpath{\n          to, t,\n          n::duration_t{std::max(n::duration_t::rep{1}, duration.count())}});\n    }\n  }\n\n  utl::sort(fps);\n\n  utl::equal_ranges_linear(\n      fps, [](auto const& a, auto const& b) { return a.target_ == b.target_; },\n      [&](std::vector<n::td_footpath>::iterator& lb,\n          std::vector<n::td_footpath>::iterator& ub) {\n        for (auto it = lb; it != ub; ++it) {\n          if (it->duration_ == n::footpath::kMaxDuration && it != lb &&\n              (it - 1)->duration_ != n::footpath::kMaxDuration) {\n            // TODO support feasible, but longer paths\n            it->valid_from_ -= (it - 1)->duration_ - n::duration_t{1U};\n          }\n        }\n      });\n\n  return fps;\n}\n\nvoid update_rtt_td_footpaths(\n    osr::ways const& w,\n    osr::lookup const& l,\n    osr::platforms const& pl,\n    nigiri::timetable const& tt,\n    point_rtree<n::location_idx_t> const& loc_rtree,\n    elevators const& e,\n    platform_matches_t const& matches,\n    hash_set<std::pair<n::location_idx_t, osr::direction>> const& tasks,\n    nigiri::rt_timetable const* old_rtt,\n    nigiri::rt_timetable& rtt,\n    std::chrono::seconds const max) {\n  auto in_mutex = std::mutex{}, out_mutex = std::mutex{};\n  auto out = std::map<n::location_idx_t, std::vector<n::td_footpath>>{};\n  auto in = std::map<n::location_idx_t, std::vector<n::td_footpath>>{};\n  utl::parallel_for_run_threadlocal<osr::bitvec<osr::node_idx_t>>(\n      tasks.size(),\n      [&](osr::bitvec<osr::node_idx_t>& blocked, std::size_t const task_idx) {\n        auto const [start, dir] = *(begin(tasks) + task_idx);\n        auto fps = get_td_footpaths(w, l, pl, tt, &rtt, loc_rtree, e, matches,\n                                    start, get_loc(tt, w, pl, matches, start),\n                                    dir, osr::search_profile::kWheelchair, max,\n                                    kMaxWheelchairMatchingDistance,\n                                    osr_parameters{}, blocked);\n        {\n          auto const lock = std::unique_lock{\n              dir == osr::direction::kForward ? out_mutex : in_mutex};\n          (dir == osr::direction::kForward ? out : in)[start] = std::move(fps);\n        }\n      });\n\n  rtt.td_footpaths_out_[2].clear();\n  for (auto i = n::location_idx_t{0U}; i != tt.n_locations(); ++i) {\n    auto const it = out.find(i);\n    if (it != end(out)) {\n      rtt.has_td_footpaths_out_[2].set(i, true);\n      rtt.td_footpaths_out_[2].emplace_back(it->second);\n    } else if (old_rtt != nullptr) {\n      rtt.has_td_footpaths_out_[2].set(\n          i, old_rtt->has_td_footpaths_out_[2].test(i));\n      rtt.td_footpaths_out_[2].emplace_back(old_rtt->td_footpaths_out_[2][i]);\n    } else {\n      rtt.has_td_footpaths_out_[2].set(i, false);\n      rtt.td_footpaths_out_[2].emplace_back(\n          std::initializer_list<n::td_footpath>{});\n    }\n  }\n\n  rtt.td_footpaths_in_[2].clear();\n  for (auto i = n::location_idx_t{0U}; i != tt.n_locations(); ++i) {\n    auto const it = in.find(i);\n    if (it != end(in)) {\n      rtt.has_td_footpaths_in_[2].set(i, true);\n      rtt.td_footpaths_in_[2].emplace_back(it->second);\n    } else if (old_rtt != nullptr) {\n      rtt.has_td_footpaths_in_[2].set(i,\n                                      old_rtt->has_td_footpaths_in_[2].test(i));\n      rtt.td_footpaths_in_[2].emplace_back(old_rtt->td_footpaths_in_[2][i]);\n    } else {\n      rtt.has_td_footpaths_in_[2].set(i, false);\n      rtt.td_footpaths_in_[2].emplace_back(\n          std::initializer_list<n::td_footpath>{});\n    }\n  }\n}\n\nvoid update_rtt_td_footpaths(osr::ways const& w,\n                             osr::lookup const& l,\n                             osr::platforms const& pl,\n                             nigiri::timetable const& tt,\n                             point_rtree<n::location_idx_t> const& loc_rtree,\n                             elevators const& e,\n                             elevator_footpath_map_t const& elevators_in_paths,\n                             platform_matches_t const& matches,\n                             nigiri::rt_timetable& rtt,\n                             std::chrono::seconds const max) {\n  auto tasks = hash_set<std::pair<n::location_idx_t, osr::direction>>{};\n  for (auto const& [e_in_path, from_to] : elevators_in_paths) {\n    auto const e_idx =\n        match_elevator(e.elevators_rtree_, e.elevators_, w, e_in_path);\n    if (e_idx == elevator_idx_t::invalid()) {\n      continue;\n    }\n    auto const& el = e.elevators_[e_idx];\n    if (el.out_of_service_.empty() && el.status_) {\n      continue;\n    }\n    for (auto const& [from, to] : from_to) {\n      tasks.emplace(from, osr::direction::kForward);\n      tasks.emplace(to, osr::direction::kBackward);\n    }\n  }\n  update_rtt_td_footpaths(w, l, pl, tt, loc_rtree, e, matches, tasks, nullptr,\n                          rtt, max);\n}\n\n}  // namespace motis\n"
  },
  {
    "path": "test/combinations_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"date/date.h\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/elevators/get_state_changes.h\"\n\nusing namespace date;\nusing namespace std::chrono_literals;\nusing namespace motis;\nnamespace n = nigiri;\n\nstd::ostream& operator<<(std::ostream& out, std::vector<bool> const& v) {\n  auto first = true;\n  for (auto const b : v) {\n    if (!first) {\n      out << \", \";\n    }\n    first = false;\n    out << std::boolalpha << b;\n  }\n  return out;\n}\n\nTEST(motis, int_state_changes) {\n  auto const changes = std::vector<std::vector<state_change<int>>>{\n      {{{0, true}, {10, false}, {20, true}}},\n      {{{0, false},\n        {5, true},\n        {10, false},\n        {15, true},\n        {20, false},\n        {25, true},\n        {30, false}}}};\n  auto g = get_state_changes(changes);\n  auto const expected = std::array<std::pair<int, std::vector<bool>>, 7>{\n      std::pair<int, std::vector<bool>>{0, {true, false}},\n      std::pair<int, std::vector<bool>>{5, {true, true}},\n      std::pair<int, std::vector<bool>>{10, {false, false}},\n      std::pair<int, std::vector<bool>>{15, {false, true}},\n      std::pair<int, std::vector<bool>>{20, {true, false}},\n      std::pair<int, std::vector<bool>>{25, {true, true}},\n      std::pair<int, std::vector<bool>>{30, {true, false}}};\n  auto i = 0U;\n  while (g) {\n    EXPECT_EQ(expected[i++], g());\n  }\n  EXPECT_EQ(i, expected.size());\n}"
  },
  {
    "path": "test/config_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/config.h\"\n\nusing namespace motis;\nusing namespace std::string_literals;\n\nTEST(motis, config) {\n  auto const c = config{\n      .osm_ = {\"europe-latest.osm.pbf\"},\n      .tiles_ = {{.profile_ = \"deps/tiles/profile/profile.lua\"}},\n      .timetable_ = {config::timetable{\n          .first_day_ = \"2024-10-02\",\n          .num_days_ = 2U,\n          .datasets_ =\n              {{\"de\",\n                {.path_ = \"delfi.gtfs.zip\",\n                 .clasz_bikes_allowed_ = {{{\"LONG_DISTANCE\", false},\n                                           {\"REGIONAL_FAST\", true}}},\n                 .rt_ =\n                     {{{.url_ =\n                            R\"(https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf)\",\n                        .headers_ = {{{\"Authorization\", \"test\"}}}}}}}},\n               {\"nl\",\n                {.path_ = \"nl.gtfs.zip\",\n                 .rt_ =\n                     {{{.url_ = R\"(https://gtfs.ovapi.nl/nl/trainUpdates.pb)\"},\n                       {.url_ =\n                            R\"(https://gtfs.ovapi.nl/nl/tripUpdates.pb)\"}}}}}},\n          .assistance_times_ = {\"assistance.csv\"}}},\n      .street_routing_ = true,\n      .limits_ = config::limits{},\n      .osr_footpath_ = true,\n      .geocoding_ = true};\n\n  EXPECT_EQ(fmt::format(R\"(\nosm: europe-latest.osm.pbf\ntiles:\n  profile: deps/tiles/profile/profile.lua\n  db_size: 274877906944\n  flush_threshold: 100000\ntimetable:\n  first_day: 2024-10-02\n  num_days: 2\n  tb: false\n  railviz: true\n  with_shapes: true\n  adjust_footpaths: true\n  merge_dupes_intra_src: false\n  merge_dupes_inter_src: false\n  link_stop_distance: 100\n  update_interval: 60\n  http_timeout: 30\n  canned_rt: false\n  incremental_rt_update: false\n  use_osm_stop_coordinates: false\n  extend_missing_footpaths: false\n  max_footpath_length: 15\n  max_matching_distance: 25.000000\n  preprocess_max_matching_distance: 250.000000\n  datasets:\n    de:\n      path: delfi.gtfs.zip\n      default_bikes_allowed: false\n      default_cars_allowed: false\n      extend_calendar: false\n      clasz_bikes_allowed:\n        LONG_DISTANCE: false\n        REGIONAL_FAST: true\n      rt:\n        - url: https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf\n          headers:\n            Authorization: test\n          protocol: gtfsrt\n    nl:\n      path: nl.gtfs.zip\n      default_bikes_allowed: false\n      default_cars_allowed: false\n      extend_calendar: false\n      rt:\n        - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb\n          protocol: gtfsrt\n        - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb\n          protocol: gtfsrt\n  assistance_times: assistance.csv\nelevators: false\nstreet_routing: true\nlimits:\n  stoptimes_max_results: 256\n  plan_max_results: 256\n  plan_max_search_window_minutes: 5760\n  stops_max_results: 2048\n  onetomany_max_many: 128\n  onetoall_max_results: 65535\n  onetoall_max_travel_minutes: 90\n  routing_max_timeout_seconds: 90\n  gtfsrt_expose_max_trip_updates: 100\n  street_routing_max_prepost_transit_seconds: 3600\n  street_routing_max_direct_seconds: 21600\n  geocode_max_suggestions: 10\n  reverse_geocode_max_results: 5\nosr_footpath: true\ngeocoding: true\nreverse_geocoding: false\n)\",\n                        std::thread::hardware_concurrency()),\n            (std::stringstream{} << \"\\n\"\n                                 << c << \"\\n\")\n                .str());\n\n  EXPECT_EQ(c, config::read(R\"(\nosm: europe-latest.osm.pbf\ntiles:\n  profile: deps/tiles/profile/profile.lua\ntimetable:\n  first_day: 2024-10-02\n  num_days: 2\n  tb: false\n  datasets:\n    de:\n      path: delfi.gtfs.zip\n      clasz_bikes_allowed:\n        LONG_DISTANCE: false\n        REGIONAL_FAST: true\n      rt:\n        - url: https://stc.traines.eu/mirror/german-delfi-gtfs-rt/latest.gtfs-rt.pbf\n          headers:\n            Authorization: test\n    nl:\n      path: nl.gtfs.zip\n      default_bikes_allowed: false\n      default_cars_allowed: false\n      extend_calendar: false\n      rt:\n        - url: https://gtfs.ovapi.nl/nl/trainUpdates.pb\n        - url: https://gtfs.ovapi.nl/nl/tripUpdates.pb\n  assistance_times: assistance.csv\nelevators: false\nstreet_routing: true\nosr_footpath: true\ngeocoding: true\n)\"s));\n\n  EXPECT_TRUE(c.use_street_routing());\n\n  // Using street_routing struct\n  {\n    // Setting height_data_dir\n    {\n      auto const street_routing_config =\n          config{.osm_ = {\"europe-latest.osm.pbf\"},\n                 .street_routing_ =\n                     config::street_routing{.elevation_data_dir_ = \"srtm/\"},\n                 .limits_ = config::limits{}};\n      EXPECT_EQ(street_routing_config, config::read(R\"(\nstreet_routing:\n  elevation_data_dir: srtm/\nosm: europe-latest.osm.pbf\n)\"s));\n      EXPECT_TRUE(street_routing_config.use_street_routing());\n    }\n\n    // Using empty street_routing map\n    {\n      auto const street_routing_config =\n          config{.osm_ = {\"europe-latest.osm.pbf\"},\n                 .street_routing_ = config::street_routing{},\n                 .limits_ = config::limits{}};\n      EXPECT_EQ(street_routing_config, config::read(R\"(\nstreet_routing: {}\nosm: europe-latest.osm.pbf\n)\"s));\n      EXPECT_TRUE(street_routing_config.use_street_routing());\n    }\n\n    // No street_routing defined\n    EXPECT_FALSE(config::read(R\"(\nosm: europe-latest.osm.pbf\n)\"s)\n                     .use_street_routing());\n\n    // street_routing disabled\n    EXPECT_FALSE(config::read(R\"(\nosm: europe-latest.osm.pbf\nstreet_routing: false\n)\"s)\n                     .use_street_routing());\n\n    // Will throw if street_routing is set but osm is not\n    EXPECT_ANY_THROW(config::read(R\"(\nstreet_routing: {}\n)\"s));\n  }\n}\n"
  },
  {
    "path": "test/elevators/parse_elevator_id_osm_mapping_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <string_view>\n\n#include \"motis/elevators/parse_elevator_id_osm_mapping.h\"\n\nusing namespace motis;\nusing namespace std::string_view_literals;\n\nconstexpr auto const kElevatorIdOsmMappingCsv = R\"__(dhid,diid,osm_kind,osm_id\ndbinfrago-temporary:d23340e4-ca1a-533e-803a-c036883147a3,diid:02b2be0f-c1da-1eef-a490-a02c488737ac,node,8891093860\nde:01002:49320,diid:02b2be0f-c1da-1eef-a490-ddb6f99637ae,node,2505371425\nde:01002:49320,diid:02b2be0f-c1da-1eef-a490-dfa6e17997ae,node,2505371422\nde:01003:57774,diid:02b2be0f-c1da-1eef-a490-aec6aa7b37ad,node,2543654133\nde:01004:66023,diid:02b2be0f-c1da-1eef-a490-a8aaa8ac17ac,node,3833491147\n)__\"sv;\n\nTEST(motis, parse_elevator_id_osm_mapping) {\n  auto const map = parse_elevator_id_osm_mapping(kElevatorIdOsmMappingCsv);\n\n  ASSERT_EQ(5, map.size());\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-a02c488737ac\", map.at(8891093860ULL));\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-ddb6f99637ae\", map.at(2505371425ULL));\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-dfa6e17997ae\", map.at(2505371422ULL));\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-aec6aa7b37ad\", map.at(2543654133ULL));\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-a8aaa8ac17ac\", map.at(3833491147ULL));\n}\n"
  },
  {
    "path": "test/elevators/parse_fasta_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/elevators/parse_fasta.h\"\n\nusing namespace date;\nusing namespace std::chrono_literals;\nusing namespace std::string_view_literals;\nusing namespace motis;\nnamespace n = nigiri;\n\nconstexpr auto const kFastaJson = R\"__(\n[\n  {\n    \"description\": \"FFM HBF zu Gleis 101/102 (S-Bahn)\",\n    \"equipmentnumber\" : 10561326,\n    \"geocoordX\" : 8.6628995,\n    \"geocoordY\" : 50.1072933,\n    \"operatorname\" : \"DB InfraGO\",\n    \"state\" : \"ACTIVE\",\n    \"stateExplanation\" : \"available\",\n    \"stationnumber\" : 1866,\n    \"type\" : \"ELEVATOR\",\n    \"outOfService\": [[\"2024-07-18T12:00:00Z\", \"2024-07-19T12:00:00Z\"]]\n  },\n  {\n    \"description\": \"FFM HBF zu Gleis 103/104 (S-Bahn)\",\n    \"equipmentnumber\": 10561327,\n    \"geocoordX\": 8.6627516,\n    \"geocoordY\": 50.1074549,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1866,\n    \"type\": \"ELEVATOR\",\n    \"outOfService\": [\n      [\"2024-07-18T11:00:00Z\", \"2024-07-18T14:00:00Z\"],\n      [\"2024-07-19T11:00:00Z\", \"2024-07-19T14:00:00Z\"]\n    ]\n  },\n  {\n    \"description\": \"FFM HBF zu Gleis 103/104 (S-Bahn)\",\n    \"equipmentnumber\": 10561327,\n    \"geocoordX\": 8.6627516,\n    \"geocoordY\": 50.1074549,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"INACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1866,\n    \"type\": \"ELEVATOR\"\n  }\n]\n)__\"sv;\n\nTEST(motis, parse_fasta) {\n  auto const elevators = parse_fasta(kFastaJson);\n  ASSERT_EQ(3, elevators.size());\n  ASSERT_EQ(1, elevators[elevator_idx_t{0}].out_of_service_.size());\n  ASSERT_EQ(2, elevators[elevator_idx_t{1}].out_of_service_.size());\n  ASSERT_EQ(0, elevators[elevator_idx_t{2}].out_of_service_.size());\n  EXPECT_EQ((n::interval<n::unixtime_t>{sys_days{2024_y / July / 18} + 12h,\n                                        sys_days{2024_y / July / 19} + 12h}),\n            elevators[elevator_idx_t{0}].out_of_service_[0]);\n  EXPECT_EQ((n::interval<n::unixtime_t>{sys_days{2024_y / July / 18} + 11h,\n                                        sys_days{2024_y / July / 18} + 14h}),\n            elevators[elevator_idx_t{1}].out_of_service_[0]);\n  EXPECT_EQ((n::interval<n::unixtime_t>{sys_days{2024_y / July / 19} + 11h,\n                                        sys_days{2024_y / July / 19} + 14h}),\n            elevators[elevator_idx_t{1}].out_of_service_[1]);\n  EXPECT_EQ((std::vector<state_change<n::unixtime_t>>{\n                {n::unixtime_t{n::unixtime_t::duration{0}}, true},\n                {sys_days{2024_y / July / 18} + 12h, false},\n                {sys_days{2024_y / July / 19} + 12h, true}}),\n            elevators[elevator_idx_t{0}].state_changes_);\n  EXPECT_EQ((std::vector<state_change<n::unixtime_t>>{\n                {n::unixtime_t{n::unixtime_t::duration{0}}, true},\n                {sys_days{2024_y / July / 18} + 11h, false},\n                {sys_days{2024_y / July / 18} + 14h, true},\n                {sys_days{2024_y / July / 19} + 11h, false},\n                {sys_days{2024_y / July / 19} + 14h, true}}),\n            elevators[elevator_idx_t{1}].state_changes_);\n\n  auto const expected =\n      std::array<std::pair<n::unixtime_t, std::vector<bool>>, 7>{\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              n::unixtime_t{n::unixtime_t::duration{0}}, {true, true, false}},\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              sys_days{2024_y / July / 18} + 11h, {true, false, false}},\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              sys_days{2024_y / July / 18} + 12h, {false, false, false}},\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              sys_days{2024_y / July / 18} + 14h, {false, true, false}},\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              sys_days{2024_y / July / 19} + 11h, {false, false, false}},\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              sys_days{2024_y / July / 19} + 12h, {true, false, false}},\n          std::pair<n::unixtime_t, std::vector<bool>>{\n              sys_days{2024_y / July / 19} + 14h, {true, true, false}}};\n  auto const single_state_changes = utl::to_vec(\n      elevators, [&](elevator const& e) { return e.get_state_changes(); });\n  auto g = get_state_changes(single_state_changes);\n  auto i = 0U;\n  while (g) {\n    auto const x = g();\n    ASSERT_LT(i, expected.size());\n    EXPECT_EQ(expected[i], x) << \"i=\" << i;\n    ++i;\n  }\n  EXPECT_EQ(expected.size(), i);\n}"
  },
  {
    "path": "test/elevators/parse_siri_fm_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <string_view>\n\n#include \"motis/elevators/parse_siri_fm.h\"\n\nusing namespace motis;\n\nconstexpr auto kSiriFm = R\"(\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Siri xmlns=\"http://www.siri.org.uk/siri\" xmlns:ns2=\"http://www.ifopt.org.uk/acsb\"\n    xmlns:ns3=\"http://www.ifopt.org.uk/ifopt\" xmlns:ns4=\"http://datex2.eu/schema/2_0RC1/2_0\"\n    xmlns:ns5=\"http://www.opengis.net/gml/3.2\" version=\"siri:2.2\">\n    <ServiceDelivery>\n        <ResponseTimestamp>2026-02-21T22:06:02Z</ResponseTimestamp>\n        <ProducerRef>dbinfrago</ProducerRef>\n        <FacilityMonitoringDelivery version=\"epiprt:2.1\">\n            <ResponseTimestamp>2026-02-21T22:06:02Z</ResponseTimestamp>\n            <FacilityCondition>\n                <FacilityRef>diid:02aeed9c-f8a3-1fd0-bceb-153a94e98000</FacilityRef>\n                <FacilityStatus>\n                    <Status>unknown</Status>\n                    <Description xml:lang=\"en\">not monitored</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-d36ed2aeb7ae</FacilityRef>\n                <FacilityStatus>\n                    <Status>available</Status>\n                    <Description xml:lang=\"en\">available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-a458663cb7ac</FacilityRef>\n                <FacilityStatus>\n                    <Status>available</Status>\n                    <Description xml:lang=\"en\">available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-9efb1b6717ac</FacilityRef>\n                <FacilityStatus>\n                    <Status>available</Status>\n                    <Description xml:lang=\"en\">available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-9d577a00b7ac</FacilityRef>\n                <FacilityStatus>\n                    <Status>available</Status>\n                    <Description xml:lang=\"en\">available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-b4967b7cf7ae</FacilityRef>\n                <FacilityStatus>\n                    <Status>available</Status>\n                    <Description xml:lang=\"en\">available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-db549df337ae</FacilityRef>\n                <FacilityStatus>\n                    <Status>notAvailable</Status>\n                    <Description xml:lang=\"en\">not available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n      </FacilityMonitoringDelivery>\n  </ServiceDelivery>\n</Siri>\n)\";\n\nTEST(motis, parse_siri_fm) {\n  auto const elevators = parse_siri_fm(std::string_view{kSiriFm});\n  ASSERT_EQ(7, elevators.size());\n\n  ASSERT_TRUE(elevators[elevator_idx_t{0}].id_str_.has_value());\n  ASSERT_TRUE(elevators[elevator_idx_t{1}].id_str_.has_value());\n  ASSERT_TRUE(elevators[elevator_idx_t{2}].id_str_.has_value());\n  ASSERT_TRUE(elevators[elevator_idx_t{3}].id_str_.has_value());\n  ASSERT_TRUE(elevators[elevator_idx_t{4}].id_str_.has_value());\n  ASSERT_TRUE(elevators[elevator_idx_t{5}].id_str_.has_value());\n  ASSERT_TRUE(elevators[elevator_idx_t{6}].id_str_.has_value());\n  EXPECT_EQ(\"diid:02aeed9c-f8a3-1fd0-bceb-153a94e98000\",\n            *elevators[elevator_idx_t{0}].id_str_);\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-d36ed2aeb7ae\",\n            *elevators[elevator_idx_t{1}].id_str_);\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-a458663cb7ac\",\n            *elevators[elevator_idx_t{2}].id_str_);\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-9efb1b6717ac\",\n            *elevators[elevator_idx_t{3}].id_str_);\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-9d577a00b7ac\",\n            *elevators[elevator_idx_t{4}].id_str_);\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-b4967b7cf7ae\",\n            *elevators[elevator_idx_t{5}].id_str_);\n  EXPECT_EQ(\"diid:02b2be0f-c1da-1eef-a490-db549df337ae\",\n            *elevators[elevator_idx_t{6}].id_str_);\n\n  EXPECT_FALSE(elevators[elevator_idx_t{0}].status_);\n  EXPECT_TRUE(elevators[elevator_idx_t{1}].status_);\n  EXPECT_TRUE(elevators[elevator_idx_t{2}].status_);\n  EXPECT_TRUE(elevators[elevator_idx_t{3}].status_);\n  EXPECT_TRUE(elevators[elevator_idx_t{4}].status_);\n  EXPECT_TRUE(elevators[elevator_idx_t{5}].status_);\n  EXPECT_FALSE(elevators[elevator_idx_t{6}].status_);\n\n  for (auto const& e : elevators) {\n    EXPECT_TRUE(e.id_str_.has_value());\n    EXPECT_TRUE(e.out_of_service_.empty());\n    EXPECT_EQ(1, e.state_changes_.size());\n    EXPECT_EQ(e.status_, e.state_changes_.front().state_);\n  }\n}\n"
  },
  {
    "path": "test/elevators/siri_fm_routing_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <chrono>\n\n#ifdef NO_DATA\n#undef NO_DATA\n#endif\n#include \"gtfsrt/gtfs-realtime.pb.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/import.h\"\n\nnamespace json = boost::json;\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\nusing namespace std::chrono_literals;\nnamespace n = nigiri;\n\nconstexpr auto const kElevatorIdOsm = R\"(dhid,diid,osm_kind,osm_id\nde:06412:10,diid:02b2be0f-c1da-1eef-a490-d5f7573837ae,node,3945358489\n)\";\n\nconstexpr auto const kSiriFm = R\"__(\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Siri xmlns=\"http://www.siri.org.uk/siri\" xmlns:ns2=\"http://www.ifopt.org.uk/acsb\"\n    xmlns:ns3=\"http://www.ifopt.org.uk/ifopt\" xmlns:ns4=\"http://datex2.eu/schema/2_0RC1/2_0\"\n    xmlns:ns5=\"http://www.opengis.net/gml/3.2\" version=\"siri:2.2\">\n    <ServiceDelivery>\n        <ResponseTimestamp>2026-02-21T22:06:02Z</ResponseTimestamp>\n        <ProducerRef>dbinfrago</ProducerRef>\n        <FacilityMonitoringDelivery version=\"epiprt:2.1\">\n            <ResponseTimestamp>2026-02-21T22:06:02Z</ResponseTimestamp>\n            <FacilityCondition>\n                <FacilityRef>diid:02b2be0f-c1da-1eef-a490-d5f7573837ae</FacilityRef>\n                <FacilityStatus>\n                    <Status>notAvailable</Status>\n                    <Description xml:lang=\"en\">not available</Description>\n                </FacilityStatus>\n            </FacilityCondition>\n      </FacilityMonitoringDelivery>\n  </ServiceDelivery>\n</Siri>\n)__\"sv;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3,DB,S3,,,109\nU4,DB,U4,,,402\nICE,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3,S1,S3,,\nU4,S1,U4,,\nICE,S1,ICE,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nS3,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0\nU4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nU4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0\nICE,00:35:00,00:35:00,DA_10,0,0,0\nICE,00:45:00,00:45:00,FFM_10,1,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n\n# frequencies.txt\ntrip_id,start_time,end_time,headway_secs\nS3,01:15:00,25:15:00,3600\nICE,00:35:00,24:35:00,3600\nU4,01:05:00,25:01:00,3600\n)\"sv;\n\nvoid print_short(std::ostream& out, api::Itinerary const& j);\nstd::string to_str(std::vector<api::Itinerary> const& x);\n\nTEST(motis, siri_fm_routing) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c =\n      config{.server_ = {{.web_folder_ = \"ui/build\", .n_threads_ = 1U}},\n             .osm_ = {\"test/resources/test_case.osm.pbf\"},\n             .tiles_ = {{.profile_ = \"deps/tiles/profile/full.lua\",\n                         .db_size_ = 1024U * 1024U * 25U}},\n             .timetable_ =\n                 config::timetable{\n                     .first_day_ = \"2019-05-01\",\n                     .num_days_ = 2,\n                     .preprocess_max_matching_distance_ = 0.0,\n                     .datasets_ = {{\"test\", {.path_ = std::string{kGTFS}}}}},\n             .elevators_ =\n                 config::elevators{.init_ = std::string{kSiriFm},\n                                   .osm_mapping_ = std::string{kElevatorIdOsm}},\n             .street_routing_ = true,\n             .osr_footpath_ = true,\n             .geocoding_ = true,\n             .reverse_geocoding_ = true};\n  import(c, \"test/data\");\n\n  auto d = data{\"test/data\", c};\n\n  auto const routing = utl::init_from<ep::routing>(d).value();\n\n  // Route with wheelchair.\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87263,8.63127\"\n        \"&toPlace=50.11347,8.67664\"\n        \"&time=2019-05-01T01:25Z\"\n        \"&pedestrianProfile=WHEELCHAIR\"\n        \"&useRoutedTransfers=true\"\n        \"&timetableView=false\");\n    EXPECT_EQ(0U, res.itineraries_.size());\n  }\n\n  // Route w/o wheelchair.\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87263,8.63127\"\n        \"&toPlace=50.11347,8.67664\"\n        \"&time=2019-05-01T01:25Z\"\n        \"&useRoutedTransfers=true\"\n        \"&timetableView=false\");\n    EXPECT_EQ(1U, res.itineraries_.size());\n  }\n}\n"
  },
  {
    "path": "test/endpoints/map_routes_test.cc",
    "content": "#include \"gmock/gmock-matchers.h\"\n#include \"gtest/gtest.h\"\n\n#include <chrono>\n#include <cstddef>\n\n#include \"boost/json.hpp\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/not_found_exception.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/map/routes.h\"\n#include \"motis/gbfs/update.h\"\n#include \"motis/import.h\"\n\nnamespace json = boost::json;\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\nusing namespace std::chrono_literals;\nusing namespace testing;\nnamespace n = nigiri;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nTest,Test,https://example.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon\nDA_Bus_1,DA Hbf,49.8724891,8.6281994\nDA_Bus_2,DA Hbf,49.8750407,8.6312172\nDA_Tram_1,DA Hbf,49.8742551,8.6321063\nDA_Tram_2,DA Hbf,49.8731133,8.6313674\nDA_Tram_3,DA Hbf,49.872435,8.632164\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_type\nB1,Test,B1,,3\nT1,Test,T1,,0\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign\nB1,S1,B1,Bus 1,\nT1,S1,T1,Tram 1,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence\nB1,01:00:00,01:00:00,DA_Bus_1,1\nB1,01:10:00,01:10:00,DA_Bus_2,2\nT1,01:05:00,01:05:00,DA_Tram_1,1\nT1,01:15:00,01:15:00,DA_Tram_2,2\nT1,01:20:00,01:20:00,DA_Tram_3,3\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n)\";\n\nTEST(motis, map_routes) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = config{\n      .osm_ = {\"test/resources/test_case.osm.pbf\"},\n      .timetable_ =\n          config::timetable{\n              .first_day_ = \"2019-05-01\",\n              .num_days_ = 2,\n              .with_shapes_ = true,\n              .datasets_ = {{\"test\", {.path_ = kGTFS}}},\n              .route_shapes_ = {{.mode_ =\n                                     config::timetable::route_shapes::mode::all,\n                                 .cache_db_size_ = 1024U * 1024U * 5U}}},\n      .street_routing_ = true};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n\n  auto const map_routes = utl::init_from<ep::routes>(d).value();\n\n  {\n    auto const res = map_routes(\n        \"/api/experimental/map/routes\"\n        \"?max=49.88135900212875%2C8.60917200508915\"\n        \"&min=49.863844157325886%2C8.649823169526556\"\n        \"&zoom=16\");\n    EXPECT_EQ(res.routes_.size(), 2U);\n    EXPECT_EQ(res.zoomFiltered_, false);\n\n    EXPECT_THAT(res.routes_, Contains(Field(&api::RouteInfo::mode_,\n                                            Eq(api::ModeEnum::BUS))));\n    EXPECT_THAT(res.routes_, Contains(Field(&api::RouteInfo::mode_,\n                                            Eq(api::ModeEnum::TRAM))));\n    EXPECT_THAT(res.routes_, Each(Field(&api::RouteInfo::pathSource_,\n                                        Eq(api::RoutePathSourceEnum::ROUTED))));\n    EXPECT_FALSE(res.polylines_.empty());\n    EXPECT_FALSE(res.stops_.empty());\n\n    for (auto const& route : res.routes_) {\n      for (auto const& segment : route.segments_) {\n        EXPECT_GE(segment.from_, 0);\n        EXPECT_GE(segment.to_, 0);\n        EXPECT_GE(segment.polyline_, 0);\n        EXPECT_LT(segment.from_, static_cast<std::int64_t>(res.stops_.size()));\n        EXPECT_LT(segment.to_, static_cast<std::int64_t>(res.stops_.size()));\n        EXPECT_LT(segment.polyline_,\n                  static_cast<std::int64_t>(res.polylines_.size()));\n      }\n    }\n\n    for (auto route_index = 0U; route_index != res.routes_.size();\n         ++route_index) {\n      for (auto const& segment : res.routes_[route_index].segments_) {\n        EXPECT_THAT(\n            res.polylines_.at(static_cast<std::size_t>(segment.polyline_))\n                .routeIndexes_,\n            Contains(static_cast<std::int64_t>(route_index)));\n      }\n    }\n  }\n\n  {\n    // map section without data\n    auto const res = map_routes(\n        \"/api/experimental/map/routes\"\n        \"?max=53.5757876577963%2C9.904453881311966\"\n        \"&min=53.518462458295005%2C10.04877290275494\"\n        \"&zoom=14.5\");\n    EXPECT_EQ(res.routes_.size(), 0U);\n    EXPECT_EQ(res.zoomFiltered_, false);\n  }\n}\n"
  },
  {
    "path": "test/endpoints/ojp_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <cstdlib>\n#include <filesystem>\n#include <fstream>\n#include <string>\n#include <string_view>\n\n#include \"date/date.h\"\n\n#include \"utl/init_from.h\"\n#include \"utl/read_file.h\"\n\n#include \"adr/formatter.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/ojp.h\"\n#include \"motis/import.h\"\n\nusing namespace motis;\nusing namespace date;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3,DB,S3,,,109\nU4,DB,U4,,,402\nICE,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3,S1,S3,,\nU4,S1,U4,,\nICE,S1,ICE,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nS3,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0\nU4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nU4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0\nICE,00:35:00,00:35:00,DA_10,0,0,0\nICE,00:45:00,00:45:00,FFM_10,1,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n\n# frequencies.txt\ntrip_id,start_time,end_time,headway_secs\nS3,01:15:00,25:15:00,3600\nICE,00:35:00,24:35:00,3600\nU4,01:05:00,25:01:00,3600\n)\";\n\nTEST(motis, ojp_requests) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c =\n      config{.osm_ = {\"test/resources/test_case.osm.pbf\"},\n             .timetable_ =\n                 config::timetable{.first_day_ = \"2019-05-01\",\n                                   .num_days_ = 2,\n                                   .datasets_ = {{\"test\", {.path_ = kGTFS}}}},\n             .street_routing_ = true,\n             .osr_footpath_ = true,\n             .geocoding_ = true};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n  d.init_rtt(date::sys_days{2019_y / May / 1});\n\n  auto const ojp_ep = ep::ojp{\n      .routing_ep_ = utl::init_from<ep::routing>(d),\n      .geocoding_ep_ = utl::init_from<ep::geocode>(d),\n      .stops_ep_ = utl::init_from<ep::stops>(d),\n      .stop_times_ep_ = utl::init_from<ep::stop_times>(d),\n      .trip_ep_ = utl::init_from<ep::trip>(d),\n  };\n\n  auto const send_request = [&](std::string_view body) {\n    net::web_server::http_req_t req{boost::beast::http::verb::post,\n                                    \"/api/v2/ojp\", 11};\n    req.set(boost::beast::http::field::content_type, \"text/xml; charset=utf-8\");\n    req.body() = std::string{body};\n    req.prepare_payload();\n    return ojp_ep(net::route_request{std::move(req)}, false);\n  };\n\n  auto const normalize_response = [](std::string_view input) {\n    auto out = std::string{input};\n\n    auto const normalize_tag = [&](std::string_view start_tag,\n                                   std::string_view end_tag,\n                                   std::string_view replacement) {\n      auto pos = std::size_t{0};\n      while ((pos = out.find(start_tag, pos)) != std::string::npos) {\n        auto const value_start = pos + start_tag.size();\n        auto const value_end = out.find(end_tag, value_start);\n        if (value_end == std::string::npos) {\n          break;\n        }\n        out.replace(value_start, value_end - value_start, replacement);\n        pos = value_start + replacement.size() + end_tag.size();\n      }\n    };\n\n    normalize_tag(\"<siri:ResponseTimestamp>\", \"</siri:ResponseTimestamp>\",\n                  \"NOW\");\n    normalize_tag(\"<siri:ResponseMessageIdentifier>\",\n                  \"</siri:ResponseMessageIdentifier>\", \"MSG\");\n    normalize_tag(\"<LinkProjection>\", \"</LinkProjection>\", \"\");\n\n    return out;\n  };\n\n  auto const expect_response = [&](char const* request_path,\n                                   char const* response_path) {\n    auto const request = utl::read_file(request_path).value();\n    auto expected = utl::read_file(response_path).value();\n    auto const reply = send_request(request);\n    auto const* res = std::get_if<net::web_server::string_res_t>(&reply);\n    ASSERT_NE(nullptr, res);\n    EXPECT_EQ(boost::beast::http::status::ok, res->result());\n    EXPECT_EQ(\"text/xml; charset=utf-8\",\n              res->base()[boost::beast::http::field::content_type]);\n    EXPECT_EQ(normalize_response(expected), normalize_response(res->body()));\n  };\n\n  expect_response(\"test/resources/ojp/geocoding_request.xml\",\n                  \"test/resources/ojp/geocoding_response.xml\");\n  expect_response(\"test/resources/ojp/map_stops_request.xml\",\n                  \"test/resources/ojp/map_stops_response.xml\");\n  expect_response(\"test/resources/ojp/stop_event_request.xml\",\n                  \"test/resources/ojp/stop_event_response.xml\");\n  expect_response(\"test/resources/ojp/trip_info_request.xml\",\n                  \"test/resources/ojp/trip_info_response.xml\");\n  expect_response(\"test/resources/ojp/routing_request.xml\",\n                  \"test/resources/ojp/routing_response.xml\");\n  expect_response(\"test/resources/ojp/intermodal_routing_request.xml\",\n                  \"test/resources/ojp/intermodal_routing_response.xml\");\n}\n"
  },
  {
    "path": "test/endpoints/one_to_many_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#ifdef NO_DATA\n#undef NO_DATA\n#endif\n\n#include <chrono>\n#include <optional>\n#include <vector>\n\n#include \"utl/init_from.h\"\n\n#include \"nigiri/common/parse_time.h\"\n\n#include \"motis-api/motis-api.h\"\n\n#include \"motis/config.h\"\n#include \"motis/endpoints/one_to_many.h\"\n#include \"motis/endpoints/one_to_many_post.h\"\n\n#include \"../test_case.h\"\n\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\nusing namespace std::chrono_literals;\n\nnamespace n = nigiri;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\nPAUL1,Römer/Paulskirche,50.110979,8.682276,0,,\nPAUL2,Römer/Paulskirche,50.110828,8.681587,0,,\nFFM_C,FFM C,50.107812,8.664628,0,,\nFFM_B,FFM B,50.107519,8.664775,0,,\nDA_Bus_1,DA Hbf,49.8722160,8.6282315\nDA_Bus_2,DA Hbf,49.8755778,8.6240518\nDA_Tram_1,DA Hbf,49.8752926,8.6277460\nDA_Tram_2,DA Hbf,49.874995,8.6313925\nDA_Tram_3,DA Hbf,49.871561,8.6320181\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3,DB,S3,,,109\nU4,DB,U4,,,402\nICE,DB,ICE,,,101\n11_1,DB,11,,,0\n11_2,DB,11,,,0\nB1,DB,B1,,3\nT1,DB,T1,,0\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3,S1,S3,,block_1\nU4,S1,U4,,block_1\nICE,S1,ICE,,\n11_1,S1,11_1_1,,\n11_1,S1,11_1_2,,\n11_2,S1,11_2_1,,\n11_2,S1,11_2_2,,\nB1,S1,B1,Bus 1,\nT1,S1,T1,Tram 1,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nS3,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0\nU4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nU4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0\nICE,00:35:00,00:35:00,DA_10,0,0,0\nICE,00:45:00,00:45:00,FFM_10,1,0,0\n11_1_1,12:00:00,12:00:00,PAUL1,0,0,0\n11_1_1,12:10:00,12:10:00,FFM_C,1,0,0\n11_1_2,12:15:00,12:15:00,PAUL1,0,0,0\n11_1_2,12:25:00,12:25:00,FFM_C,1,0,0\n11_2_1,12:05:00,12:05:00,FFM_B,0,0,0\n11_2_1,12:15:00,12:15:00,PAUL2,1,0,0\n11_2_2,12:20:00,12:20:00,FFM_B,0,0,0\n11_2_2,12:30:00,12:30:00,PAUL2,1,0,0\nB1,00:10:00,00:10:00,DA_Bus_1,1\nB1,00:20:00,00:20:00,DA_Bus_2,2\nT1,00:24:00,00:24:00,DA_Tram_1,1\nT1,00:25:00,00:25:00,DA_Tram_2,2\nT1,00:26:00,00:26:00,DA_Tram_3,3\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n)\";\n\ntemplate <>\ntest_case_params const import_test_case<test_case::FFM_one_to_many>() {\n  auto const c =\n      config{.osm_ = {\"test/resources/test_case.osm.pbf\"},\n             .timetable_ =\n                 config::timetable{.first_day_ = \"2019-05-01\",\n                                   .num_days_ = 2,\n                                   .datasets_ = {{\"test\", {.path_ = kGTFS}}}},\n             .street_routing_ = true,\n             .osr_footpath_ = true};\n  return import_test_case(std::move(c), \"test/test_case/ffm_one_to_many_data\");\n}\n\nstd::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>\nparse_time(std::string_view time) {\n  return std::chrono::time_point_cast<std::chrono::seconds>(\n      n::parse_time(time, \"%FT%T%Ez\"));\n}\n\nauto one_to_many_get(data& d) {\n  return utl::init_from<ep::one_to_many_intermodal>(d).value();\n}\n\nauto one_to_many_post(data& d) {\n  return utl::init_from<ep::one_to_many_intermodal_post>(d).value();\n}\n\nTEST(one_to_many, get_request_forward) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_get(d)(\n      \"/api/experimental/one-to-many-intermodal\"\n      \"?one=49.8722439;8.6320624\"  // Near DA\n      \"&many=\"\n      \"49.87336;8.62926,\"  // DA_10\n      \"50.10593;8.66118,\"  // FFM_10\n      \"50.107577;8.6638173,\"  // de:6412:10:6:1\n      \"50.10739;8.66333,\"  // FFM_101\n      \"50.11385;8.67912,\"  // FFM_HAUPT_U\n      \"50.11404;8.67824,\"  // FFM_HAUPT_S\n      \"49.872855;8.632008,\"  // Near one\n      \"49.872504;8.628988,\"  // Inside DA station\n      \"49.874399;8.630361,\"  // Still reachable, near DA\n      \"49.875292;8.627746,\"  // Far, near DA\n      \"50.106596;8.663485,\"  // Inside FFM station\n      \"50.106209;8.668934,\"  // Outside FFM station\n      \"50.103663;8.663666,\"  // Far, not reachable, near FFM\n      \"50.113494;8.679129,\"  // Near FFM_HAUPT_U\n      \"50.114080;8.677027,\"  // Near FFM_HAUPT_S\n      \"50.114520;8.673050,\"  // Too far from FFM_HAUPT_U\n      \"50.114773;8.672604\"  // Far, near FFM_HAUPT\n      \"&time=2019-04-30T22:30:00.000Z\"\n      \"&maxTravelTime=60\"\n      \"&maxMatchingDistance=250\"\n      \"&maxDirectTime=540\"  // Updated to maxPreTransitTime == 1500\n      \"&maxPostTransitTime=420\"\n      \"&arriveBy=false\");\n\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = {{{.duration_ = 281.0},\n                                       {},\n                                       {},\n                                       {},\n                                       {},\n                                       {},\n                                       {.duration_ = 122.0},\n                                       {.duration_ = 240.0},\n                                       {.duration_ = 529.0},\n                                       {.duration_ = 582.0},\n                                       {},\n                                       {},\n                                       {},\n                                       {},\n                                       {},\n                                       {},\n                                       {}}},\n                .transit_durations_ = {{\n                    {},\n                    {{.duration_ = 1080.0, .transfers_ = 0}},\n                    {// Not routed transfer => faster than realistic\n                     {.duration_ = 1140.0, .transfers_ = 0}},\n                    {// Not routed transfer\n                     {.duration_ = 1140.0, .transfers_ = 0}},\n                    {{.duration_ = 2580.0, .transfers_ = 1}},\n                    {{.duration_ = 2580.0, .transfers_ = 1}},\n                    {},\n                    {},\n                    {},\n                    {},\n                    {{.duration_ = 1260.0, .transfers_ = 0}},\n                    {{.duration_ = 1620.0, .transfers_ = 0}},\n                    {},\n                    {{.duration_ = 2700.0, .transfers_ = 1}},\n                    {{.duration_ = 2640.0, .transfers_ = 1}},\n                    {{.duration_ = 2940.0, .transfers_ = 1}},\n                    {},\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, post_request_backward) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"50.113816,8.679421,0\",  // Near FFM_HAUPT\n      .many_ =\n          {\n              \"49.87336,8.62926\",  // DA_10\n              \"50.10593,8.66118\",  // FFM_10\n              \"test_FFM_10\",\n              \"50.107577,8.6638173\",  // de:6412:10:6:1\n              \"50.10739,8.66333\",  // FFM_101\n              \"test_FFM_101\",\n              \"50.11385,8.67912\",  // FFM_HAUPT_U\n              \"50.11385,8.67912,-4\",  // FFM_HAUPT_U\n              \"test_FFM_HAUPT_U\",\n              \"50.11404,8.67824\",  // FFM_HAUPT_S\n              \"50.113385,8.678328,0\",  // Close, near FFM_HAUPT, level 0\n              \"50.113385,8.678328,-2\",  // Close, near FFM_HAUPT, level -2\n              \"50.111900,8.675208\",  // Far, near FFM_HAUPT\n              \"50.106543,8.663474,0\",  // Close, near FFM\n              \"50.107361,8.660478\",  // Too far from de:6412:10:6:1\n              \"50.104298,8.660285\",  // Far, near FFM\n              \"49.872243,8.632062\",  // Near DA\n              \"49.875368,8.627596\",  // Far, near DA\n          },\n      .time_ = parse_time(\"2019-05-01T01:25:00.000+02:00\"),\n      .maxTravelTime_ = 60,\n      .maxMatchingDistance_ = 250.0,\n      .arriveBy_ = true,\n      .maxPreTransitTime_ = 300,\n      .maxDirectTime_ = 300});  // Updated to maxPostTransitTime == 1500\n\n  EXPECT_EQ(\n      (api::OneToManyIntermodalResponse{\n          .street_durations_ = {{\n              {},\n              {},\n              {},\n              {},\n              {},\n              {},\n              {.duration_ = 159.0},  // No explicit level\n              {.duration_ = 160.0},  // Explicit level\n              {.duration_ = 160.0},\n              {.duration_ = 127.0},\n              {.duration_ = 103.0},\n              {.duration_ = 123.0},\n              {.duration_ = 355.0},\n              {},\n              {},\n              {},\n              {},\n              {},\n          }},\n          .transit_durations_ = {{\n              {{.duration_ = 3180.0, .transfers_ = 1}},\n              {{.duration_ = 840.0, .transfers_ = 0}},  // Not routed transfer\n              {{.duration_ = 720.0, .transfers_ = 0}},  // Not routed transfer\n              {{.duration_ = 780.0, .transfers_ = 0}},\n              {{.duration_ = 780.0, .transfers_ = 0}},\n              {{.duration_ = 720.0, .transfers_ = 0}},\n              {},\n              {},\n              {},\n              {},\n              {},\n              {},\n              {},\n              {{.duration_ = 900.0, .transfers_ = 0}},\n              {{.duration_ = 1020.0, .transfers_ = 0}},\n              {},\n              {{.duration_ = 3360.0, .transfers_ = 1}},  // from DA_10: 3420.0\n              {},\n          }}}),\n      durations);\n}\n\nTEST(one_to_many,\n     post_request_forward_with_routed_transfers_and_short_pre_transit) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"50.106691,8.659763\",  // Near FFM\n      .many_ =\n          {\n              \"test_DA_10\",\n              \"50.107577,8.6638173\",  // de:6412:10:6:1\n              \"test_de:6412:10:6:1\",\n              \"test_FFM_101\",\n              \"test_FFM_HAUPT_S\",\n              \"50.11385,8.67912\",  // FFM_HAUPT_U\n              \"50.10590,8.66452\",  // Near FFM\n              \"50.113291,8.678321,0\",  // Near FFM_HAUPT\n              \"50.113127,8.678879,-2\",  // Near FFM_HAUPT\n              \"50.114141,8.677025,-3\",  // Near FFM_HAUPT\n              \"50.113589,8.679070,-4\",  // Near FFM_HAUPT\n          },\n      .time_ = parse_time(\"2019-05-01T00:55:00.000+02:00\"),\n      .maxTravelTime_ = 60,\n      .maxMatchingDistance_ = 250.0,\n      .arriveBy_ = false,\n      .useRoutedTransfers_ = true,\n      .maxPreTransitTime_ = 360});  // Too short to reach U4\n\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = {{\n                    {},\n                    {.duration_ = 443.0},\n                    {.duration_ = 370.0},  // Direct connection allowed\n                    {.duration_ = 321.0},  // Valid for pre transit\n                    {},\n                    {},\n                    {.duration_ = 403.0},\n                    {},\n                    {},\n                    {},\n                    {},\n                }},\n                .transit_durations_ = {{\n                    {},\n                    {},\n                    {},\n                    {},\n                    {{.duration_ = 1560.0, .transfers_ = 0}},  // Must take S3\n                    {{.duration_ = 1680.0, .transfers_ = 0}},  // Must take S3\n                    {},\n                    {{.duration_ = 1800.0, .transfers_ = 0}},\n                    {{.duration_ = 1740.0, .transfers_ = 0}},\n                    {{.duration_ = 1740.0, .transfers_ = 0}},\n                    {{.duration_ = 1680.0, .transfers_ = 0}},\n\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, get_request_backward_with_wheelchair_and_short_post_transit) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_get(d)(\n      \"/api/experimental/one-to-many-intermodal\"\n      \"?one=50.11385;8.67912\"  // FFM_HAUPT_U\n      \"&many=\"\n      \"50.107577;8.6638173,\"  // de:6412:10:6:1\n      \"50.10739;8.66333,\"  // FFM_101\n      \"50.11404;8.67824,\"  // FFM_HAUPT_S\n      \"50.113465;8.678477,\"  // Near FFM_HAUPT\n      \"50.112519;8.676565\"  // Far, near FFM_HAUPT\n      \"&time=2019-04-30T23:30:00.000Z\"\n      \"&maxTravelTime=60\"\n      \"&maxMatchingDistance=250\"\n      \"&maxDirectTime=540\"\n      \"&maxPostTransitTime=240\"\n      \"&pedestrianProfile=WHEELCHAIR\"\n      \"&useRoutedTransfers=true\"\n      \"&withDistance=true\"\n      \"&arriveBy=true\");\n\n  auto const& sd = durations.street_durations_.value();\n  auto const& td = durations.transit_durations_.value();\n\n  ASSERT_EQ(5U, sd.size());\n  EXPECT_EQ(api::Duration{}, sd.at(0));\n  EXPECT_EQ(api::Duration{}, sd.at(1));\n  // Not valid for post transit => unreachable from FFM_101\n  EXPECT_DOUBLE_EQ(333.0, sd.at(2).duration_.value());\n  EXPECT_NEAR(124.1, sd.at(2).distance_.value(), 0.1);\n  EXPECT_DOUBLE_EQ(517.0, sd.at(3).duration_.value());\n  EXPECT_NEAR(271.8, sd.at(3).distance_.value(), 0.1);\n  EXPECT_DOUBLE_EQ(771.0, sd.at(4).duration_.value());\n  EXPECT_NEAR(476.0, sd.at(4).distance_.value(), 0.1);\n\n  ASSERT_EQ(5U, td.size());\n  ASSERT_EQ(1U, td.at(0).size());\n  EXPECT_DOUBLE_EQ(1680.0, td.at(0).at(0).duration_);\n  EXPECT_EQ(0, td.at(0).at(0).transfers_);\n  // Unreachable, as FFM_HAUPT_S -> FFM_HAUPT_U not usable postTransit\n  EXPECT_TRUE(td.at(1).empty());\n  EXPECT_TRUE(td.at(2).empty());\n  EXPECT_TRUE(td.at(3).empty());\n  EXPECT_TRUE(td.at(4).empty());\n}\n\nTEST(one_to_many, oneway_get_forward_for_pre_transit_and_direct_modes) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_get(d)(\n      \"/api/experimental/one-to-many-intermodal\"\n      \"?one=50.107328;8.664836\"\n      \"&many=\"\n      \"50.107812;8.664628,\"  // FFM C  (shorter path)\n      \"50.107519;8.664775,\"  // FFM B  (longer path, due to oneway)\n      \"50.110828;8.681587\"  // PAUL2\n      \"&time=2019-05-01T10:00:00.00Z\"\n      \"&maxTravelTime=60\"\n      \"&maxMatchingDistance=250\"\n      \"&maxDirectTime=3600\"\n      \"&directMode=BIKE\"\n      \"&preTransitModes=BIKE\"\n      \"&arriveBy=false\"\n      \"&cyclingSpeed=2.4\");\n\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = {{\n                    {.duration_ = 228.0},\n                    {.duration_ = 321.0},\n                    {},\n                }},\n                .transit_durations_ = {{\n                    {},\n                    {},\n                    {// Must use later trip\n                     {.duration_ = 1980.0, .transfers_ = 0}},\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, oneway_post_backward_for_post_transit_and_direct_modes) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"50.107326,8.665237\",\n      .many_ = {\"test_FFM_B\", \"test_FFM_C\", \"50.107812,8.664628\", \"test_PAUL1\"},\n      .time_ = parse_time(\"2019-05-01T12:30:00.000+02:00\"),\n      .maxTravelTime_ = 60,\n      .maxMatchingDistance_ = 250.0,\n      .arriveBy_ = true,\n      .cyclingSpeed_ = 2.2,\n      .postTransitModes_ = {api::ModeEnum::BIKE},\n      .directMode_ = api::ModeEnum::BIKE,\n      .withDistance_ = true});\n\n  auto const& sd = durations.street_durations_.value();\n  auto const& td = durations.transit_durations_.value();\n\n  ASSERT_EQ(4U, sd.size());\n  EXPECT_DOUBLE_EQ(228.0, sd.at(0).duration_.value());\n  EXPECT_NEAR(341.3, sd.at(0).distance_.value(), 0.1);\n  EXPECT_DOUBLE_EQ(335.0, sd.at(1).duration_.value());\n  EXPECT_NEAR(502.1, sd.at(1).distance_.value(), 0.1);\n  EXPECT_DOUBLE_EQ(335.0, sd.at(2).duration_.value());\n  EXPECT_NEAR(502.1, sd.at(2).distance_.value(), 0.1);\n  EXPECT_EQ(api::Duration{}, sd.at(3));\n\n  ASSERT_EQ(4U, td.size());\n  EXPECT_TRUE(td.at(0).empty());\n  EXPECT_TRUE(td.at(1).empty());\n  EXPECT_TRUE(td.at(2).empty());\n  ASSERT_EQ(1U, td.at(3).size());\n  EXPECT_DOUBLE_EQ(1920.0, td.at(3).at(0).duration_);\n  EXPECT_EQ(0, td.at(3).at(0).transfers_);\n}\n\nTEST(one_to_many, oneway_post_forward_for_post_transit_modes) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"test_PAUL1\",\n      .many_ = {\"test_FFM_C\", \"50.107326,8.665237\"},  // includes C -> B\n      .time_ = parse_time(\"2019-05-01T12:00:00.000+02:00\"),\n      .maxTravelTime_ = 30,\n      .maxMatchingDistance_ = 250.0,\n      .arriveBy_ = false,\n      .postTransitModes_ = {api::ModeEnum::BIKE}});\n\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = std::vector<api::Duration>(2),\n                .transit_durations_ = {{\n                    {{.duration_ = 720.0, .transfers_ = 0}},\n                    {{.duration_ = 840.0, .transfers_ = 0}},\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, oneway_get_backward_for_pre_transit_modes) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_get(d)(\n      \"/api/experimental/one-to-many-intermodal\"\n      \"?one=50.110828;8.681587\"  // PAUL2\n      \"&many=\"\n      \"50.107812;8.664628,\"  // FFM C  (with incorrect transfer C -> B)\n      \"50.107519;8.664775,\"  // FFM B\n      \"50.107328;8.664836\"  // Long preTransit due to oneway  (C -> B)\n      \"&time=2019-05-01T10:20:00.00Z\"\n      \"&maxTravelTime=60\"\n      \"&maxMatchingDistance=250\"\n      \"&preTransitModes=BIKE\"\n      \"&arriveBy=true\"\n      \"&cyclingSpeed=2.4\");\n\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = std::vector<api::Duration>(3),\n                .transit_durations_ = {{\n                    {{.duration_ = 1080.0, .transfers_ = 0}},\n                    {{.duration_ = 1080.0, .transfers_ = 0}},\n                    {{.duration_ = 1260.0, .transfers_ = 0}},\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, transfer_time_settings_min_transfer_time) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_get(d)(\n      \"/api/experimental/one-to-many-intermodal\"\n      \"?one=49.872710;8.631168\"  // Near DA\n      \"&many=50.113487;8.678913\"  // Near FFM_HAUPT\n      \"&time=2019-04-30T22:30:00.00Z\"\n      \"&useRoutedTransfers=true\"\n      \"&minTransferTime=21\");\n\n  // FIXME Times are also added for final footpath\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = {{api::Duration{}}},\n                .transit_durations_ = {{\n                    {{.duration_ = 4320.0, .transfers_ = 1}},\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, transfer_time_settings_additional_transfer_time) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"49.872710, 8.631168\",  // Near DA\n      .many_ = {\"50.113487, 8.678913\"},  // Near FFM_HAUPT\n      .time_ = parse_time(\"2019-05-01T00:30:00.000+02:00\"),\n      .additionalTransferTime_ = 17,\n      .useRoutedTransfers_ = true});\n\n  // FIXME Times are also added for final footpath\n  EXPECT_EQ((api::OneToManyIntermodalResponse{\n                .street_durations_ = {{api::Duration{}}},\n                .transit_durations_ = {{\n                    {{.duration_ = 4200.0, .transfers_ = 1}},\n                }}}),\n            durations);\n}\n\nTEST(one_to_many, bug_additional_footpath_for_first_last_mile) {\n  // Bug examples: Should not connect final footpath with first or last mile\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const ep = one_to_many_post(d);\n  auto const many = std::vector<std::string>{\n      \"test_FFM_HAUPT_U\", \"50.11385,8.67912,-4\",  // FFM_HAUPT_U\n      \"test_FFM_HAUPT_S\", \"50.11404,8.67824,-3\",  // FFM_HAUPT_S\n      \"50.114093,8.676546\"};  // Test location\n  {\n    // Last location should not be reachable when only arriving with U4\n    auto const durations = ep(api::OneToManyIntermodalParams{\n        .one_ = \"50.108056,8.663177,-2\",  // Near de:6412:10:6:1\n        .many_ = many,\n        .time_ = parse_time(\"2019-05-01T01:00:00.000+02:00\"),\n        .maxTravelTime_ = 30,\n        .maxMatchingDistance_ = 250.0,\n        .arriveBy_ = false,\n        .useRoutedTransfers_ = true,\n        .pedestrianProfile_ = api::PedestrianProfileEnum::WHEELCHAIR,\n        .maxPostTransitTime_ = 420});  // Too short to reach from U4\n\n    EXPECT_EQ((api::OneToManyIntermodalResponse{\n                  .street_durations_ = std::vector<api::Duration>(5),\n                  .transit_durations_ = {{\n                      {{.duration_ = 720.0, .transfers_ = 0}},\n                      {{.duration_ = 780.0, .transfers_ = 0}},\n                      {{.duration_ = 720.0, .transfers_ = 0}},\n                      {{.duration_ = 1020.0, .transfers_ = 0}},\n                      {// FIXME Test location should be unreachable\n                       {.duration_ = 1380.0, .transfers_ = 0}},\n                  }}}),\n              durations);\n  }\n\n  {\n    // Test that location is reachable from FFM_HAUPT_S after arrival\n    auto const test_durations = ep(api::OneToManyIntermodalParams{\n        .one_ = \"50.10739,8.66333,-3\",  // Near FFM_101\n        .many_ = many,\n        .time_ = parse_time(\"2019-05-01T01:00:00.000+02:00\"),\n        .maxTravelTime_ = 30,\n        .maxMatchingDistance_ = 250.0,\n        .arriveBy_ = false,\n        .useRoutedTransfers_ = true,\n        .pedestrianProfile_ = api::PedestrianProfileEnum::WHEELCHAIR,\n        .maxPostTransitTime_ = 420});  // Reachable from S3\n    EXPECT_EQ((api::OneToManyIntermodalResponse{\n                  .street_durations_ = std::vector<api::Duration>(5),\n                  .transit_durations_ = {{\n                      {{.duration_ = 1260.0, .transfers_ = 0}},\n                      {{.duration_ = 1620.0, .transfers_ = 0}},\n                      {{.duration_ = 1260.0, .transfers_ = 0}},\n                      {{.duration_ = 1380.0, .transfers_ = 0}},\n                      {{.duration_ = 1740.0, .transfers_ = 0}},\n                  }}}),\n              test_durations);\n  }\n\n  {\n    // Ensure all arriving stations are reachable\n    // Also check that the correct arrival time is used\n    // Should start from FFM_HAUPT_S due to post transit time constraint\n    auto const walk_durations = ep(api::OneToManyIntermodalParams{\n        .one_ = \"50.107066,8.663604,0\",\n        .many_ = many,\n        .time_ = parse_time(\"2019-05-01T01:00:00.000+02:00\"),\n        .maxTravelTime_ = 30,\n        .maxMatchingDistance_ = 250.0,\n        .arriveBy_ = false,\n        .useRoutedTransfers_ = true,\n        .pedestrianProfile_ = api::PedestrianProfileEnum::FOOT,\n        .maxPostTransitTime_ = 240});  // Only reachable from S3\n    EXPECT_EQ((api::OneToManyIntermodalResponse{\n                  .street_durations_ = std::vector<api::Duration>(5),\n                  .transit_durations_ = {{\n                      {{.duration_ = 720.0, .transfers_ = 0}},\n                      {{.duration_ = 780.0, .transfers_ = 0}},\n                      {{.duration_ = 720.0, .transfers_ = 0}},\n                      {{.duration_ = 780.0, .transfers_ = 0}},\n                      {// FIXME Should start FFM_HAUPT_S => time > 1200\n                       {.duration_ = 960.0, .transfers_ = 0}},\n                  }}}),\n              walk_durations);\n  }\n}\n\nTEST(one_to_many, pareto_sets_with_routed_transfers_and_distances) {\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"49.8722160,8.6282315\",  // DA_Bus_1\n      .many_ = {\"49.875292,8.6277460\",  // near Tram_1, must be south of node\n                \"49.874995,8.6313925\",  // near Tram_2\n                \"49.871561,8.6320181\",  // near Tram_3\n                \"50.111900,8.675208\"},  // near FFM_HAUPT\n      .time_ = parse_time(\"2019-05-01T00:05:00.000+02:00\"),\n      .useRoutedTransfers_ = true,\n      .withDistance_ = true});\n\n  auto const& sd = durations.street_durations_.value();\n  auto const& td = durations.transit_durations_.value();\n\n  ASSERT_EQ(4U, sd.size());\n  EXPECT_DOUBLE_EQ(344.0, sd.at(0).duration_.value());\n  EXPECT_NEAR(351.9, sd.at(0).distance_.value(), 0.1);\n  EXPECT_DOUBLE_EQ(556.0, sd.at(1).duration_.value());\n  EXPECT_NEAR(607.0, sd.at(1).distance_.value(), 0.1);\n  EXPECT_DOUBLE_EQ(966.0, sd.at(2).duration_.value());\n  EXPECT_NEAR(1100.6, sd.at(2).distance_.value(), 0.1);\n  EXPECT_EQ(api::Duration{}, sd.at(3));\n\n  ASSERT_EQ(4U, td.size());\n  ASSERT_EQ(1U, td.at(0).size());\n  EXPECT_DOUBLE_EQ(1320.0, td.at(0).at(0).duration_);\n  EXPECT_EQ(0, td.at(0).at(0).transfers_);\n  ASSERT_EQ(1U, td.at(1).size());\n  EXPECT_DOUBLE_EQ(1680.0, td.at(1).at(0).duration_);\n  EXPECT_EQ(0, td.at(1).at(0).transfers_);\n  ASSERT_EQ(1U, td.at(2).size());\n  EXPECT_DOUBLE_EQ(1740.0, td.at(2).at(0).duration_);\n  EXPECT_EQ(0, td.at(2).at(0).transfers_);\n  ASSERT_EQ(1U, td.at(3).size());\n  EXPECT_DOUBLE_EQ(4440.0, td.at(3).at(0).duration_);\n  EXPECT_EQ(2, td.at(3).at(0).transfers_);\n}\n\nTEST(one_to_many, pareto_sets_with_multiple_entries) {\n  // Long walking paths + fast connctions => multiple durations\n  // Currently: Long transfer times, so that transit is faster\n  // After bug fix: Slow walking speed, so that transit is faster\n  // might require moving stops (B2->T1, T1->T2, T2 delete) with paths:\n  // Bus1 -> Tram3, Bus1 -> Bus2 -> Tram3, Bus1 -> Bus2 -> Tram1/2 -> Tram3\n  auto [d, _config] = get_test_case<test_case::FFM_one_to_many>();\n\n  auto const durations = one_to_many_post(d)(api::OneToManyIntermodalParams{\n      .one_ = \"49.8722160,8.6282315\",  // DA_Bus_1\n      .many_ = {\"49.8755778,8.6240518\",  // DA_Bus_2\n                \"49.8752926,8.6277460\",  // DA_Tram_1\n                \"49.871561,8.6320181\"},  // DA_Tram_3\n      .time_ = parse_time(\"2019-05-01T00:05:00.000+02:00\"),\n      .maxPreTransitTime_ = 300});  // Prevent any pre transit to Tram_x\n\n  // We only care about duration to DA_Tram_3, everything else is for debugging\n  auto const& sd = durations.street_durations_.value();\n  ASSERT_EQ(3U, sd.size());\n  EXPECT_DOUBLE_EQ(966.0, sd.at(2).duration_.value());\n  EXPECT_EQ((std::optional<std::vector<std::vector<api::ParetoSetEntry>>>{{\n                {{.duration_ = 1080.0, .transfers_ = 0}},\n                {{.duration_ = 1140.0, .transfers_ = 0}},\n                {{.duration_ = 1500.0, .transfers_ = 0},\n                 {.duration_ = 1440.0, .transfers_ = 1}},\n            }}),\n            durations.transit_durations_);\n}\n"
  },
  {
    "path": "test/endpoints/siri_sx_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <algorithm>\n#include <filesystem>\n\n#include \"date/date.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/trip.h\"\n#include \"motis/import.h\"\n#include \"motis/rt/auser.h\"\n#include \"motis/tag_lookup.h\"\n\nusing namespace motis;\nusing namespace date;\n\nconstexpr auto const kSiriSxGtfs = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nTEST,Test Agency,https://example.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nSTOP1,Stop 1,48.0,9.0,0,,\nSTOP2,Stop 2,48.1,9.1,0,,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nR1,TEST,R1,,,3\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nR1,S1,T1,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nT1,10:00:00,10:00:00,STOP1,1,0,0\nT1,10:10:00,10:10:00,STOP2,2,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20260110,1\n)\";\n\nconstexpr auto const kSiriSxJsonUpdate = R\"({\n  \"responseTimestamp\": \"2026-01-10T09:55:00Z\",\n  \"estimatedTimetableDelivery\": {\n    \"estimatedJourneyVersionFrame\": {\n      \"recordedAtTime\": \"2026-01-10T09:55:00Z\",\n      \"estimatedVehicleJourney\": {\n        \"lineRef\": \"R1\",\n        \"directionRef\": \"0\",\n        \"framedVehicleJourneyRef\": {\n          \"dataFrameRef\": \"20260110\",\n          \"datedVehicleJourneyRef\": \"40-1-24290-78300\"\n        },\n        \"estimatedCalls\": {\n          \"estimatedCall\": [\n            {\n              \"stopPointRef\": {\n                \"value\": \"STOP1\"\n              },\n              \"order\": 1,\n              \"extraCall\": false,\n              \"cancellation\": false,\n              \"aimedArrivalTime\": \"2026-01-10T10:00:00+01:00\",\n              \"aimedDepartureTime\": \"2026-01-10T10:00:00+01:00\"\n            },\n            {\n              \"stopPointRef\": {\n                \"value\": \"STOP2\"\n              },\n              \"order\": 2,\n              \"extraCall\": false,\n              \"cancellation\": false,\n              \"aimedArrivalTime\": \"2026-01-10T10:10:00+01:00\",\n              \"aimedDepartureTime\": \"2026-01-10T10:10:00+01:00\"\n            }\n          ]\n        }\n      }\n    }\n  },\n  \"situationExchangeDelivery\": {\n    \"situations\": {\n      \"ptSituationElement\": [\n        {\n          \"creationTime\": {\n            \"value\": \"2026-01-10T09:00:00Z\"\n          },\n          \"situationNumber\": {\n            \"value\": \"S1\"\n          },\n          \"validityPeriod\": [\n            {\n              \"startTime\": {\n                \"value\": \"2026-01-10T09:00:00Z\"\n              },\n              \"endTime\": {\n                \"value\": \"2026-01-10T12:00:00Z\"\n              }\n            }\n          ],\n          \"publicationWindow\": {\n            \"startTime\": {\n              \"value\": \"2026-01-10T09:00:00Z\"\n            }\n          },\n          \"severity\": {\n            \"value\": \"noImpact\"\n          },\n          \"affects\": {\n            \"stopPlaces\": {\n              \"affectedStopPlace\": [\n                {\n                  \"stopPlaceRef\": {\n                    \"value\": \"STOP1\"\n                  }\n                }\n              ]\n            },\n            \"affectedLines\": {\n              \"affectedLine\": [\n                {\n                  \"lineRef\": {\n                    \"value\": \"4-121-4\"\n                  }\n                },\n                {\n                  \"lineRef\": {\n                    \"value\": \"4-104-4\"\n                  }\n                }\n              ]\n            }\n          },\n          \"summary\": [\n            {\n              \"value\": \"Platform change\"\n            }\n          ],\n          \"description\": [\n            {\n              \"value\": \"Use platform 2\"\n            }\n          ]\n        },\n        {\n          \"creationTime\": {\n            \"value\": \"2026-01-10T09:05:00Z\"\n          },\n          \"situationNumber\": {\n            \"value\": \"S2\"\n          },\n          \"validityPeriod\": [\n            {\n              \"startTime\": {\n                \"value\": \"2026-01-10T09:00:00Z\"\n              },\n              \"endTime\": {\n                \"value\": \"2026-01-10T12:00:00Z\"\n              }\n            }\n          ],\n          \"publicationWindow\": {\n            \"startTime\": {\n              \"value\": \"2026-01-10T09:00:00Z\"\n            }\n          },\n          \"severity\": {\n            \"value\": \"minor\"\n          },\n          \"affects\": {\n            \"networks\": {\n              \"affectedNetwork\": [\n                {\n                  \"affectedLine\": [\n                    {\n                      \"lineRef\": {\n                        \"value\": \"R1\"\n                      }\n                    }\n                  ]\n                }\n              ]\n            }\n          },\n          \"summary\": [\n            {\n              \"value\": \"Line disruption\"\n            }\n          ],\n          \"description\": [\n            {\n              \"value\": \"R1 diverted due to works\"\n            }\n          ]\n        },\n        {\n          \"creationTime\": {\n            \"value\": \"2026-01-10T09:10:00Z\"\n          },\n          \"situationNumber\": {\n            \"value\": \"S3\"\n          },\n          \"validityPeriod\": [\n            {\n              \"startTime\": {\n                \"value\": \"2026-01-10T09:00:00Z\"\n              },\n              \"endTime\": {\n                \"value\": \"2026-01-10T12:00:00Z\"\n              }\n            }\n          ],\n          \"publicationWindow\": {\n            \"startTime\": {\n              \"value\": \"2026-01-10T09:00:00Z\"\n            }\n          },\n          \"severity\": {\n            \"value\": \"minor\"\n          },\n          \"affects\": {\n            \"vehicleJourneys\": {\n              \"affectedVehicleJourney\": [\n                {\n                  \"framedVehicleJourneyRef\": {\n                    \"dataFrameRef\": {\n                      \"value\": \"20260110\"\n                    },\n                    \"datedVehicleJourneyRef\": {\n                      \"value\": \"40-1-24290-78300\"\n                    }\n                  }\n                }\n              ]\n            }\n          },\n          \"summary\": [\n            {\n              \"value\": \"Vehicle journey issue\"\n            }\n          ],\n          \"description\": [\n            {\n              \"value\": \"Specific trip impacted\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n})\";\n\nTEST(motis, trip_siri_sx_alerts) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = config{\n      .timetable_ =\n          config::timetable{.first_day_ = \"2026-01-10\",\n                            .num_days_ = 1,\n                            .datasets_ = {{\"test\", {.path_ = kSiriSxGtfs}}}},\n      .street_routing_ = false};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n  d.init_rtt(sys_days{2026_y / January / 10});\n\n  auto& rtt = *d.rt_->rtt_;\n  auto siri_updater =\n      auser(*d.tt_, d.tags_->get_src(\"test\"),\n            nigiri::rt::vdv_aus::updater::xml_format::kSiriJson);\n  siri_updater.consume_update(kSiriSxJsonUpdate, rtt);\n\n  auto const trip_ep = utl::init_from<ep::trip>(d).value();\n  auto const res = trip_ep(\"?tripId=20260110_10%3A00_test_T1\");\n  ASSERT_EQ(1, res.legs_.size());\n  auto const& leg = res.legs_.front();\n  ASSERT_TRUE(leg.from_.alerts_.has_value());\n  ASSERT_FALSE(leg.from_.alerts_->empty());\n  EXPECT_EQ(\"Platform change\", leg.from_.alerts_->front().headerText_);\n  EXPECT_EQ(\"Use platform 2\", leg.from_.alerts_->front().descriptionText_);\n  ASSERT_TRUE(leg.alerts_.has_value());\n  auto const has_line_alert = std::any_of(\n      begin(*leg.alerts_), end(*leg.alerts_), [](api::Alert const& alert) {\n        return alert.headerText_ == \"Line disruption\" &&\n               alert.descriptionText_ == \"R1 diverted due to works\";\n      });\n  EXPECT_TRUE(has_line_alert);\n  auto const has_vehicle_alert = std::any_of(\n      begin(*leg.alerts_), end(*leg.alerts_), [](api::Alert const& alert) {\n        return alert.headerText_ == \"Vehicle journey issue\" &&\n               alert.descriptionText_ == \"Specific trip impacted\";\n      });\n  EXPECT_TRUE(has_vehicle_alert);\n}\n"
  },
  {
    "path": "test/endpoints/stop_group_geocoding_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <algorithm>\n#include <filesystem>\n\n#include \"utl/init_from.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/adr/geocode.h\"\n#include \"motis/import.h\"\n\nusing namespace motis;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station\nG1,Group 1,,0.0,0.0,,0,\nG2,Group 2,,0.0,0.0,,0,\nA,Stop A,,48.1,11.5,,0,\nB,Stop B,,48.2,11.6,,0,\nC,Stop C,,48.3,11.7,,0,\nD,Stop D,,48.4,11.8,,0,\n\n# stop_group_elements.txt\nstop_group_id,stop_id\nG1,A\nG2,B\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20200101,1\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nRB,DB,RB,,,3\nRT,DB,RT,,,0\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nRB,S1,TB,RB,\nRT,S1,TT,RT,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nTB,10:00:00,10:00:00,A,1,0,0\nTB,10:30:00,10:30:00,C,2,0,0\nTT,11:00:00,11:00:00,B,1,0,0\nTT,11:30:00,11:30:00,D,2,0,0\n)\";\n\nTEST(motis, stop_group_geocoding) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c =\n      config{.timetable_ =\n                 config::timetable{.first_day_ = \"2020-01-01\",\n                                   .num_days_ = 2,\n                                   .datasets_ = {{\"test\", {.path_ = kGTFS}}}},\n             .geocoding_ = true};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n\n  auto const geocode = utl::init_from<ep::geocode>(d).value();\n\n  auto const g1 = geocode(\"/api/v1/geocode?text=Group%201\");\n  auto const g1_it =\n      utl::find_if(g1, [](auto const& m) { return m.id_ == \"test_G1\"; });\n  ASSERT_NE(end(g1), g1_it);\n  ASSERT_TRUE(g1_it->modes_.has_value());\n  EXPECT_NE(end(*g1_it->modes_), utl::find(*g1_it->modes_, api::ModeEnum::BUS));\n  ASSERT_TRUE(g1_it->importance_.has_value());\n  EXPECT_GT(*g1_it->importance_, 0.0);\n\n  auto const g2 = geocode(\"/api/v1/geocode?text=Group%202\");\n  auto const g2_it =\n      utl::find_if(g2, [](auto const& m) { return m.id_ == \"test_G2\"; });\n  ASSERT_NE(end(g2), g2_it);\n  ASSERT_TRUE(g2_it->modes_.has_value());\n  EXPECT_NE(end(*g2_it->modes_),\n            utl::find(*g2_it->modes_, api::ModeEnum::TRAM));\n  ASSERT_TRUE(g2_it->importance_.has_value());\n  EXPECT_GT(*g2_it->importance_, 0.0);\n}\n"
  },
  {
    "path": "test/endpoints/stop_times_test.cc",
    "content": "#include \"motis/endpoints/stop_times.h\"\n#include \"gtest/gtest.h\"\n\n#include <chrono>\n#include <sstream>\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/json.hpp\"\n\n#include \"net/bad_request_exception.h\"\n#include \"net/not_found_exception.h\"\n\n#ifdef NO_DATA\n#undef NO_DATA\n#endif\n#include \"gtfsrt/gtfs-realtime.pb.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"nigiri/rt/gtfsrt_update.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/elevators/parse_fasta.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/gbfs/update.h\"\n#include \"motis/import.h\"\n\n#include \"../util.h\"\n\nnamespace json = boost::json;\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\nusing namespace std::chrono_literals;\nusing namespace test;\nnamespace n = nigiri;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3,DB,S3,,,109\nU4,DB,U4,,,402\nICE,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3,S1,S3,,block_1\nU4,S1,U4,,block_1\nICE,S1,ICE,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nU4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nU4,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:20:00,01:20:00,FFM_10,2,0,0\nICE,00:35:00,00:35:00,DA_10,0,0,0\nICE,00:45:00,00:45:00,FFM_10,1,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n)\";\n\nTEST(motis, stop_times) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = config{.timetable_ = config::timetable{\n                            .first_day_ = \"2019-05-01\",\n                            .num_days_ = 2,\n                            .datasets_ = {{\"test\", {.path_ = kGTFS}}}}};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n  d.init_rtt(date::sys_days{2019_y / May / 1});\n\n  auto const stats =\n      n::rt::gtfsrt_update_msg(\n          *d.tt_, *d.rt_->rtt_, n::source_idx_t{0}, \"test\",\n          to_feed_msg({trip_update{\n                           .trip_ = {.trip_id_ = \"ICE\",\n                                     .start_time_ = {\"00:35:00\"},\n                                     .date_ = {\"20190501\"}},\n                           .stop_updates_ = {{.stop_id_ = \"FFM_12\",\n                                              .seq_ = std::optional{1U},\n                                              .ev_type_ = n::event_type::kArr,\n                                              .delay_minutes_ = 10,\n                                              .stop_assignment_ = \"FFM_12\"}}},\n                       alert{\n                           .header_ = \"Yeah\",\n                           .description_ = \"Yeah!!\",\n                           .entities_ = {{.trip_ =\n                                              {\n                                                  {.trip_id_ = \"ICE\",\n                                                   .start_time_ = {\"00:35:00\"},\n                                                   .date_ = {\"20190501\"}},\n                                              },\n                                          .stop_id_ = \"DA\"}}},\n                       alert{.header_ = \"Hello\",\n                             .description_ = \"World\",\n                             .entities_ =\n                                 {{.trip_ = {{.trip_id_ = \"ICE\",\n                                              .start_time_ = {\"00:35:00\"},\n                                              .date_ = {\"20190501\"}}}}}}},\n                      date::sys_days{2019_y / May / 1} + 9h));\n  EXPECT_EQ(1U, stats.total_entities_success_);\n  EXPECT_EQ(2U, stats.alert_total_resolve_success_);\n\n  auto const stop_times = utl::init_from<ep::stop_times>(d).value();\n  EXPECT_EQ(d.rt_->rtt_.get(), stop_times.rt_->rtt_.get());\n\n  {\n    auto const res = stop_times(\n        \"/api/v5/stoptimes?stopId=test_FFM_10\"\n        \"&time=2019-04-30T23:30:00.000Z\"\n        \"&arriveBy=true\"\n        \"&n=3\"\n        \"&language=de\"\n        \"&fetchStops=true\");\n\n    auto const format_time = [&](auto&& t, char const* fmt = \"%F %H:%M\") {\n      return date::format(fmt, *t);\n    };\n\n    EXPECT_EQ(\"test_FFM_10\", res.place_.stopId_);\n    EXPECT_EQ(3, res.stopTimes_.size());\n\n    auto const& ice = res.stopTimes_[0];\n    EXPECT_EQ(api::ModeEnum::HIGHSPEED_RAIL, ice.mode_);\n    EXPECT_EQ(\"20190501_00:35_test_ICE\", ice.tripId_);\n    EXPECT_EQ(\"test_DA_10\", ice.tripFrom_.stopId_);\n    EXPECT_EQ(\"test_FFM_12\", ice.tripTo_.stopId_);\n    EXPECT_EQ(\"ICE\", ice.displayName_);\n    EXPECT_EQ(\"FFM Hbf\", ice.headsign_);\n    EXPECT_EQ(\"test_ICE\", ice.routeId_);\n    EXPECT_EQ(\"2019-04-30 22:55\", format_time(ice.place_.arrival_.value()));\n    EXPECT_EQ(\"2019-04-30 22:45\",\n              format_time(ice.place_.scheduledArrival_.value()));\n    EXPECT_EQ(true, ice.realTime_);\n    EXPECT_EQ(1, ice.previousStops_->size());\n    EXPECT_EQ(1, ice.place_.alerts_->size());\n\n    auto const& sbahn = res.stopTimes_[2];\n    EXPECT_EQ(\n        api::ModeEnum::SUBWAY,\n        sbahn.mode_);  // mode can't change with block_id so sticks from U4\n    EXPECT_EQ(\"20190501_01:15_test_S3\", sbahn.tripId_);\n    EXPECT_EQ(\"test_FFM_101\", sbahn.tripFrom_.stopId_);\n    EXPECT_EQ(\"test_FFM_10\", sbahn.tripTo_.stopId_);\n    EXPECT_EQ(\"S3\", sbahn.displayName_);\n    EXPECT_EQ(\"FFM Hbf\", sbahn.headsign_);\n    EXPECT_EQ(\"test_S3\", sbahn.routeId_);\n    EXPECT_EQ(\"2019-04-30 23:20\", format_time(sbahn.place_.arrival_.value()));\n    EXPECT_EQ(\"2019-04-30 23:20\",\n              format_time(sbahn.place_.scheduledArrival_.value()));\n    EXPECT_EQ(false, sbahn.realTime_);\n    EXPECT_EQ(2, sbahn.previousStops_->size());\n  }\n\n  {\n    // same test with alerts off\n    auto const res2 = stop_times(\n        \"/api/v5/stoptimes?stopId=test_FFM_10\"\n        \"&time=2019-04-30T23:30:00.000Z\"\n        \"&arriveBy=true\"\n        \"&n=3\"\n        \"&language=de\"\n        \"&fetchStops=true\"\n        \"&withAlerts=false\");\n    EXPECT_EQ(3, res2.stopTimes_.size());\n    for (auto const& stopTime : res2.stopTimes_) {\n      EXPECT_FALSE(stopTime.place_.alerts_.has_value());\n    }\n  }\n\n  {\n    // center-only query, radius is required\n    auto const res = stop_times(\n        \"/api/v5/stoptimes?center=50.10593,8.66118\"\n        \"&radius=250\"\n        \"&time=2019-04-30T23:30:00.000Z\"\n        \"&arriveBy=true\"\n        \"&n=3\"\n        \"&language=de\"\n        \"&fetchStops=true\");\n\n    EXPECT_EQ(\"center\", res.place_.name_);\n    EXPECT_FALSE(res.place_.stopId_.has_value());\n    EXPECT_FALSE(res.stopTimes_.empty());\n  }\n\n  {\n    // invalid stopId without center\n    EXPECT_THROW(stop_times(\"/api/v5/stoptimes?stopId=test_SOMETHING_RANDOM\"\n                            \"&time=2019-04-30T23:30:00.000Z\"\n                            \"&arriveBy=true\"\n                            \"&n=3\"),\n                 net::not_found_exception);\n  }\n\n  {\n    // invalid stopId should fall back to center\n    auto const res = stop_times(\n        \"/api/v5/stoptimes?stopId=test_SOMETHING_RANDOM\"\n        \"&center=50.10593,8.66118\"\n        \"&radius=250\"\n        \"&time=2019-04-30T23:30:00.000Z\"\n        \"&arriveBy=true\"\n        \"&n=3\"\n        \"&language=de\");\n\n    EXPECT_EQ(\"center\", res.place_.name_);\n    EXPECT_FALSE(res.place_.stopId_.has_value());\n    EXPECT_FALSE(res.stopTimes_.empty());\n  }\n\n  {\n    // stoptimes in radius = r\n    auto const r = 110.0;\n    auto const center = geo::latlng{50.10563, 8.66218};\n    auto const res =\n        stop_times(std::format(\"/api/v5/stoptimes?center={},{}\"\n                               \"&radius={}\"\n                               \"&exactRadius=true\"\n                               \"&time=2019-04-30T23:30:00.000Z\"\n                               \"&arriveBy=true\"\n                               \"&n=200\"\n                               \"&language=de\"\n                               \"&fetchStops=true\",\n                               center.lat_, center.lng_, r));\n    EXPECT_FALSE(res.stopTimes_.empty());\n    for (auto const& v : res.stopTimes_) {\n      auto const dist =\n          geo::distance(center, geo::latlng{v.place_.lat_, v.place_.lon_});\n      EXPECT_LE(dist, r);\n    }\n  }\n\n  {\n    // neither stopId nor center -> panic\n    EXPECT_THROW(stop_times(\"/api/v5/stoptimes?time=2019-04-30T23:30:00.000Z\"\n                            \"&arriveBy=true\"\n                            \"&n=3\"),\n                 net::bad_request_exception);\n  }\n\n  {\n    // center without stopId requires radius\n    EXPECT_THROW(stop_times(\"/api/v5/stoptimes?center=50.10593,8.66118\"\n                            \"&time=2019-04-30T23:30:00.000Z\"\n                            \"&arriveBy=true\"\n                            \"&n=3\"),\n                 net::bad_request_exception);\n  }\n\n  {\n    // window query LATER\n    auto const res = stop_times(\n        \"/api/v5/stoptimes?stopId=test_FFM_101\"\n        \"&time=2019-04-30T23:00:00.000Z\"\n        \"&arriveBy=true\"\n        \"&direction=LATER\"\n        \"&window=1800\"\n        \"&language=de\");\n\n    auto const format_time = [&](auto&& t, char const* fmt = \"%F %H:%M\") {\n      return date::format(fmt, *t);\n    };\n\n    EXPECT_EQ(2, res.stopTimes_.size());  // n is ignored if window is set\n    for (auto const& stop_time : res.stopTimes_) {\n      auto const arr = format_time(stop_time.place_.arrival_.value());\n      std::cout << \"arr: \" << arr << std::endl;\n      EXPECT_GE(arr, \"2019-04-30 23:00\");\n      EXPECT_LE(arr, \"2019-04-30 23:30\");\n    }\n    EXPECT_FALSE(res.previousPageCursor_.empty());\n    EXPECT_FALSE(res.nextPageCursor_.empty());\n  }\n  {\n    // window query EARLIER\n    auto const res = stop_times(\n        \"/api/v5/stoptimes?stopId=test_FFM_101\"\n        \"&time=2019-04-30T23:15:00.000Z\"\n        \"&arriveBy=true\"\n        \"&direction=EARLIER\"\n        \"&window=1800\"\n        \"&language=de\");\n\n    auto const format_time = [&](auto&& t, char const* fmt = \"%F %H:%M\") {\n      return date::format(fmt, *t);\n    };\n\n    for (auto const& stop_time : res.stopTimes_) {\n      auto const arr = format_time(stop_time.place_.arrival_.value());\n      std::cout << \"arr E: \" << arr << std::endl;\n      EXPECT_GE(arr, \"2019-04-30 22:45\");\n      EXPECT_LE(arr, \"2019-04-30 23:15\");\n    }\n  }\n  {\n    // window query EARLIER (small window large n)\n    auto const res = stop_times(\n        \"/api/v5/stoptimes?stopId=test_FFM_101\"\n        \"&time=2019-04-30T23:15:00.000Z\"\n        \"&arriveBy=true\"\n        \"&direction=LATER\"\n        \"&window=60\"\n        \"&n=2\"\n        \"&language=de\");\n\n    auto const format_time = [&](auto&& t, char const* fmt = \"%F %H:%M\") {\n      return date::format(fmt, *t);\n    };\n\n    EXPECT_GT(res.stopTimes_.size(), 1);\n    for (auto const& stop_time : res.stopTimes_) {\n      auto const arr = format_time(stop_time.place_.arrival_.value());\n      std::cout << \"arr E2: \" << arr << std::endl;\n    }\n  }\n}\n"
  },
  {
    "path": "test/endpoints/trip_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"nigiri/common/parse_time.h\"\n#include \"nigiri/rt/create_rt_timetable.h\"\n#include \"nigiri/rt/frun.h\"\n#include \"nigiri/rt/rt_timetable.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/endpoints/trip.h\"\n#include \"motis/import.h\"\n#include \"motis/rt/auser.h\"\n#include \"motis/tag_lookup.h\"\n\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nParent1,Parent1,50.0,8.0,1,,\nChild1A,Child1A,50.001,8.001,0,Parent1,1\nChild1B,Child1B,50.002,8.002,0,Parent1,2\nParent2,Parent2,51.0,9.0,1,,\nChild2,Child2,51.001,9.001,0,Parent2,1\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nR1,DB,R1,R1,,109\nR2,DB,R2,R2,,109\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nR1,S1,T1,Parent2 Express,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type,stop_headsign\nT1,10:00:00,10:00:00,Child1A,1,0,0,Origin\nT1,10:10:00,10:10:00,Child1B,2,0,0,Midway\nT1,11:00:00,11:00:00,Child2,3,0,0,Destination\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n\n# translations.txt\ntable_name,field_name,language,translation,record_id,record_sub_id,field_value\nroutes,route_long_name,de,DE-R1,,,R1\nroutes,route_long_name,fr,FR-R1,,,R1\nroutes,route_long_name,en,EN-R1,,,R1\nstops,stop_name,en,Child1A,Child1A,,\nstops,stop_name,de,Kind 1A,Child1A,,\nstops,stop_name,en,Child1B,,,Child1B\nstops,stop_name,de,Kind 1B,,,Child1B\nstops,stop_name,en,Parent2,Parent2,,\nstops,stop_name,de,Eltern 2,Parent2,,\nstops,stop_name,fr,Parent Deux,Parent2,,\nstops,stop_name,fr,Enfant 1A,Child1A,,\nstops,stop_name,fr,Enfant 1B,,,Child1B\nstop_times,stop_headsign,en,Parent2 Express,T1,1,\nstop_times,stop_headsign,de,Richtung Eltern Zwei,T1,1,\nstop_times,stop_headsign,fr,Vers Parent Deux,T1,1,\n)\";\n\nconstexpr auto kScript = R\"(\nfunction process_route(route)\n  route:set_short_name({\n    translation.new('en', 'EN_SHORT_NAME'),\n    translation.new('de', 'DE_SHORT_NAME'),\n    translation.new('fr', 'FR_SHORT_NAME')\n  })\n  route:get_short_name_translations():add(translation.new('hu', 'HU_SHORT_NAME'))\n  print(route:get_short_name_translations():get(1):get_text())\n  print(route:get_short_name_translations():get(1):get_language())\nend\n)\";\n\nTEST(motis, trip_stop_naming) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = config{\n      .timetable_ =\n          config::timetable{\n              .first_day_ = \"2019-05-01\",\n              .num_days_ = 2,\n              .datasets_ = {{\"test\", {.path_ = kGTFS, .script_ = kScript}}}},\n      .street_routing_ = false};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n\n  auto const trip_ep = utl::init_from<ep::trip>(d).value();\n\n  auto const res = trip_ep(\"?tripId=20190501_10%3A00_test_T1\");\n  ASSERT_EQ(1, res.legs_.size());\n  auto const& leg = res.legs_[0];\n  EXPECT_GT(leg.legGeometry_.length_, 0);\n  EXPECT_EQ(\"Child1A\", leg.from_.name_);\n  ASSERT_TRUE(leg.intermediateStops_.has_value());\n  ASSERT_EQ(1, leg.intermediateStops_->size());\n  EXPECT_EQ(\"Child1B\", leg.intermediateStops_->at(0).name_);\n  EXPECT_EQ(\"Parent2\", leg.to_.name_);\n  EXPECT_EQ(\"Parent2 Express\", leg.headsign_);\n  EXPECT_EQ(\"EN_SHORT_NAME\", leg.routeShortName_);\n  EXPECT_EQ(\"EN-R1\", leg.routeLongName_);\n\n  auto const compact_res =\n      trip_ep(\"?tripId=20190501_10%3A00_test_T1&detailedLegs=false\");\n  ASSERT_EQ(1, compact_res.legs_.size());\n  EXPECT_EQ(\"\", compact_res.legs_[0].legGeometry_.points_);\n  EXPECT_EQ(0, compact_res.legs_[0].legGeometry_.length_);\n\n  auto const res_de = trip_ep(\"?tripId=20190501_10%3A00_test_T1&language=de\");\n  ASSERT_EQ(1, res_de.legs_.size());\n  auto const& leg_de = res_de.legs_[0];\n  EXPECT_EQ(\"Kind 1A\", leg_de.from_.name_);\n  ASSERT_TRUE(leg_de.intermediateStops_.has_value());\n  ASSERT_EQ(1, leg_de.intermediateStops_->size());\n  EXPECT_EQ(\"Kind 1B\", leg_de.intermediateStops_->at(0).name_);\n  EXPECT_EQ(\"Eltern 2\", leg_de.to_.name_);\n  EXPECT_EQ(\"Richtung Eltern Zwei\", leg_de.headsign_);\n  EXPECT_EQ(\"DE_SHORT_NAME\", leg_de.routeShortName_);\n  EXPECT_EQ(\"DE-R1\", leg_de.routeLongName_);\n\n  auto const res_fr = trip_ep(\"?tripId=20190501_10%3A00_test_T1&language=fr\");\n  ASSERT_EQ(1, res_fr.legs_.size());\n  auto const& leg_fr = res_fr.legs_[0];\n  EXPECT_EQ(\"Enfant 1A\", leg_fr.from_.name_);\n  ASSERT_TRUE(leg_fr.intermediateStops_.has_value());\n  ASSERT_EQ(1, leg_fr.intermediateStops_->size());\n  EXPECT_EQ(\"Enfant 1B\", leg_fr.intermediateStops_->at(0).name_);\n  EXPECT_EQ(\"Parent Deux\", leg_fr.to_.name_);\n  EXPECT_EQ(\"Vers Parent Deux\", leg_fr.headsign_);\n  EXPECT_EQ(\"FR_SHORT_NAME\", leg_fr.routeShortName_);\n  EXPECT_EQ(\"FR-R1\", leg_fr.routeLongName_);\n}\n\nconstexpr auto kNetex = R\"(\n# netex.xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<PublicationDelivery xmlns:gml=\"http://www.opengis.net/gml/3.2\"\n                     xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n                     xmlns:siri=\"http://www.siri.org.uk/siri\"\n                     xmlns=\"http://www.netex.org.uk/netex\"\n                     version=\"1.09\"\n                     xsi:schemaLocation=\"http://www.netex.org.uk/netex http://netex.uk/netex/schema/1.10/xsd/NeTEx_publication.xsd\">\n  <PublicationTimestamp>2025-06-26T14:16:54+02:00</PublicationTimestamp>\n  <ParticipantRef>INTERMAPS</ParticipantRef>\n  <dataObjects>\n    <CompositeFrame id=\"ch:1:CompositeFrame:intermaps\" version=\"any\">\n      <FrameDefaults>\n        <DefaultLocale>\n          <TimeZoneOffset>1</TimeZoneOffset>\n          <SummerTimeZoneOffset>2</SummerTimeZoneOffset>\n          <DefaultLanguage>de</DefaultLanguage>\n        </DefaultLocale>\n      </FrameDefaults>\n      <ValidBetween>\n        <FromDate>2024-12-15T00:00:00</FromDate>\n        <ToDate>2025-12-14T23:59:59</ToDate>\n      </ValidBetween>\n      <frames>\n        <ResourceFrame id=\"ch:1:ResourceFrame:1\" version=\"any\">\n          <typesOfValue>\n            <ValueSet id=\"ch:1:ValueSet:TypeOfProductCategory\" version=\"any\" nameOfClass=\"TypeOfProductCategory\">\n              <Name>ProductCategories</Name>\n              <values>\n                <TypeOfProductCategory id=\"ch:1:TypeOfProductCategory:PB\" version=\"any\">\n                  <Name lang=\"de\">Pendelbahn</Name>\n                  <ShortName lang=\"de\">PB</ShortName>\n                </TypeOfProductCategory>\n              </values>\n            </ValueSet>\n          </typesOfValue>\n          <vehicleTypes>\n            <VehicleType id=\"ch:1:VehicleType:cabin\" version=\"any\">\n              <Name>Cabin</Name>\n              <ShortName>CBN</ShortName>\n            </VehicleType>\n          </vehicleTypes>\n          <organisations>\n            <Operator id=\"ch:1:sboid:100220\" version=\"any\">\n              <PublicCode>PB</PublicCode>\n              <Name>Test Operator</Name>\n            </Operator>\n          </organisations>\n        </ResourceFrame>\n        <ServiceCalendarFrame id=\"ch:1:ServiceCalendarFrame:ts3\" version=\"any\">\n          <validityConditions>\n            <AvailabilityCondition id=\"ch:1:AvailabilityCondition:whatever\" version=\"any\">\n              <FromDate>2024-12-15T00:00:00</FromDate>\n              <ToDate>2024-12-15T23:59:59</ToDate>\n              <ValidDayBits>1</ValidDayBits>\n            </AvailabilityCondition>\n          </validityConditions>\n        </ServiceCalendarFrame>\n        <SiteFrame id=\"ch:1:SiteFrame:1\" version=\"any\">\n          <stopPlaces>\n            <StopPlace id=\"ch:1:StopPlace:30243\" version=\"any\">\n              <keyList>\n                <KeyValue>\n                  <Key>SLOID</Key>\n                  <Value>ch:1:sloid:30243</Value>\n                </KeyValue>\n              </keyList>\n              <Name>Bettmeralp Talstation (Seilb.)</Name>\n              <Centroid>\n                <Location>\n                  <Longitude>8.1967</Longitude>\n                  <Latitude>46.3803</Latitude>\n                </Location>\n              </Centroid>\n              <quays>\n                <Quay id=\"ch:1:Quay:30243:1\" version=\"any\">\n                  <keyList>\n                    <KeyValue>\n                      <Key>SLOID</Key>\n                      <Value>ch:1:sloid:30243:0:403158</Value>\n                    </KeyValue>\n                  </keyList>\n                  <Name>Bettmeralp Talstation (Seilb.)</Name>\n                  <Centroid>\n                    <Location>\n                      <Longitude>8.1967</Longitude>\n                      <Latitude>46.3803</Latitude>\n                    </Location>\n                  </Centroid>\n                </Quay>\n              </quays>\n            </StopPlace>\n            <StopPlace id=\"ch:1:StopPlace:1954\" version=\"any\">\n              <keyList>\n                <KeyValue>\n                  <Key>SLOID</Key>\n                  <Value>ch:1:sloid:1954</Value>\n                </KeyValue>\n              </keyList>\n              <Name>Bettmeralp</Name>\n              <Centroid>\n                <Location>\n                  <Longitude>8.1977</Longitude>\n                  <Latitude>46.4219</Latitude>\n                </Location>\n              </Centroid>\n              <quays>\n                <Quay id=\"ch:1:Quay:1954:1\" version=\"any\">\n                  <keyList>\n                    <KeyValue>\n                      <Key>SLOID</Key>\n                      <Value>ch:1:sloid:1954:0:845083</Value>\n                    </KeyValue>\n                  </keyList>\n                  <Name>Bettmeralp</Name>\n                  <Centroid>\n                    <Location>\n                      <Longitude>8.1977</Longitude>\n                      <Latitude>46.4219</Latitude>\n                    </Location>\n                  </Centroid>\n                </Quay>\n              </quays>\n            </StopPlace>\n          </stopPlaces>\n        </SiteFrame>\n        <ServiceFrame id=\"ch:1:ServiceFrame:ts3\" version=\"any\">\n          <lines>\n            <Line id=\"ch:1:slnid:1024859\" version=\"any\">\n              <Name>2336 - Betten Talstation - Bettmeralp (Direkt)</Name>\n              <TransportMode>bus</TransportMode>\n              <TransportSubmode>\n                <BusSubmode>localBus</BusSubmode>\n              </TransportSubmode>\n              <TypeOfProductCategoryRef ref=\"ch:1:TypeOfProductCategory:PB\" version=\"any\" />\n              <additionalOperators>\n                <OperatorRef ref=\"ch:1:sboid:100220\" version=\"any\" />\n              </additionalOperators>\n            </Line>\n          </lines>\n          <destinationDisplays>\n            <DestinationDisplay id=\"ch:1:DestinationDisplay:alp\" version=\"any\">\n              <FrontText>Bettmeralp</FrontText>\n            </DestinationDisplay>\n          </destinationDisplays>\n          <scheduledStopPoints>\n            <ScheduledStopPoint id=\"ch:1:sloid:30243:0:403158\" version=\"any\">\n              <Name lang=\"de\">Bettmeralp Talstation (Seilb.)</Name>\n            </ScheduledStopPoint>\n            <ScheduledStopPoint id=\"ch:1:sloid:1954:0:845083\" version=\"any\">\n              <Name lang=\"de\">Bettmeralp</Name>\n            </ScheduledStopPoint>\n          </scheduledStopPoints>\n          <stopAssignments>\n            <PassengerStopAssignment id=\"ch:1:PassengerStopAssignment:1\" version=\"any\">\n              <ScheduledStopPointRef ref=\"ch:1:sloid:30243:0:403158\" version=\"any\" />\n              <QuayRef ref=\"ch:1:Quay:30243:1\" version=\"any\" />\n            </PassengerStopAssignment>\n            <PassengerStopAssignment id=\"ch:1:PassengerStopAssignment:2\" version=\"any\">\n              <ScheduledStopPointRef ref=\"ch:1:sloid:1954:0:845083\" version=\"any\" />\n              <QuayRef ref=\"ch:1:Quay:1954:1\" version=\"any\" />\n            </PassengerStopAssignment>\n          </stopAssignments>\n          <notices>\n            <Notice id=\"ch:1:Notice:A__FS\" version=\"any\">\n              <alternativeTexts>\n                <AlternativeText attributeName=\"Text\">\n                  <Text lang=\"en\">Free Internet with the SBB FreeSurf app</Text>\n                </AlternativeText>\n                <AlternativeText attributeName=\"Text\">\n                  <Text lang=\"fr\">Connexion Internet gratuite avec l'app FreeSurf CFF</Text>\n                </AlternativeText>\n                <AlternativeText attributeName=\"Text\">\n                  <Text lang=\"it\">Connessione Internet gratuita con l'app FreeSurf FFS</Text>\n                </AlternativeText>\n              </alternativeTexts>\n              <Text lang=\"de\">Gratis-Internet mit der App SBB FreeSurf</Text>\n              <ShortCode>A__FS</ShortCode>\n              <PrivateCode>A__FS</PrivateCode>\n              <TypeOfNoticeRef ref=\"ch:1:TypeOfNotice:10\" version=\"any\" />\n              <CanBeAdvertised>true</CanBeAdvertised>\n            </Notice>\n          </notices>\n        </ServiceFrame>\n        <TimetableFrame id=\"ch:1:TimetableFrame:ts3\" version=\"any\">\n          <vehicleJourneys>\n            <ServiceJourney id=\"ch:1:ServiceJourney:whatever\" version=\"any\">\n              <keyList>\n                <KeyValue>\n                  <Key>TripNr</Key>\n                  <Value>2336</Value>\n                </KeyValue>\n              </keyList>\n              <validityConditions>\n                <AvailabilityConditionRef ref=\"ch:1:AvailabilityCondition:whatever\" version=\"any\" />\n              </validityConditions>\n              <TypeOfProductCategoryRef ref=\"ch:1:TypeOfProductCategory:PB\" version=\"any\" />\n              <DepartureTime>05:50:00</DepartureTime>\n              <JourneyDuration>PT7M</JourneyDuration>\n              <OperatorRef ref=\"ch:1:sboid:100220\" version=\"any\" />\n              <VehicleTypeRef ref=\"ch:1:VehicleType:cabin\" version=\"any\" />\n              <LineRef ref=\"ch:1:slnid:1024859\" version=\"any\" />\n              <DirectionType>inbound</DirectionType>\n              <calls>\n                <Call id=\"ch:1:Call:whatever1\" version=\"any\" order=\"1\">\n                  <ScheduledStopPointRef ref=\"ch:1:sloid:30243:0:403158\" version=\"any\" />\n                  <Departure>\n                    <Time>05:50:00</Time>\n                  </Departure>\n                  <DestinationDisplayRef ref=\"ch:1:DestinationDisplay:alp\" version=\"any\" />\n                  <noticeAssignments>\n                    <NoticeAssignment id=\"ch:1:NoticeAssignment:1\" order=\"1\">\n                      <NoticeRef ref=\"ch:1:Notice:A__FS\" version=\"any\" />\n                    </NoticeAssignment>\n                  </noticeAssignments>\n                </Call>\n                <Call id=\"ch:1:Call:whatever2\" version=\"any\" order=\"2\">\n                  <ScheduledStopPointRef ref=\"ch:1:sloid:1954:0:845083\" version=\"any\" />\n                  <Arrival>\n                    <Time>05:57:00</Time>\n                  </Arrival>\n                  <DestinationDisplayRef ref=\"ch:1:DestinationDisplay:alp\" version=\"any\" />\n                  <noticeAssignments>\n                    <NoticeAssignment id=\"ch:1:NoticeAssignment:2\" order=\"1\">\n                      <NoticeRef ref=\"ch:1:Notice:A__FS\" version=\"any\" />\n                    </NoticeAssignment>\n                  </noticeAssignments>\n                </Call>\n              </calls>\n            </ServiceJourney>\n          </vehicleJourneys>\n        </TimetableFrame>\n      </frames>\n    </CompositeFrame>\n  </dataObjects>\n</PublicationDelivery>\n)\";\n\nTEST(motis, trip_notice_translations) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c =\n      config{.timetable_ =\n                 config::timetable{.first_day_ = \"2024-12-15\",\n                                   .num_days_ = 2,\n                                   .datasets_ = {{\"netex\", {.path_ = kNetex}}}},\n             .street_routing_ = false};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n  auto const day = date::sys_days{2024_y / December / 15};\n  d.init_rtt(day);\n  auto& rtt = *d.rt_->rtt_;\n\n  auto const trip_ep = utl::init_from<ep::trip>(d).value();\n  auto const base_trip = std::string{\n      \"?tripId=20241215_05%3A50_netex_ch%3A1%3AServiceJourney%3Awhatever\"};\n\n  auto const expect_notice = [&](std::optional<std::string_view> language,\n                                 std::string_view expected) {\n    auto url = base_trip;\n    if (language.has_value()) {\n      url += \"&language=\";\n      url.append(language->data(), language->size());\n    }\n    auto const res = trip_ep(url);\n    ASSERT_EQ(1, res.legs_.size());\n    auto const& leg = res.legs_[0];\n    ASSERT_TRUE(leg.alerts_.has_value());\n    ASSERT_FALSE(leg.alerts_->empty());\n    EXPECT_EQ(expected, leg.alerts_->front().headerText_);\n  };\n\n  expect_notice(std::nullopt, \"Gratis-Internet mit der App SBB FreeSurf\");\n  expect_notice(\"de\"sv, \"Gratis-Internet mit der App SBB FreeSurf\");\n  expect_notice(\"en\"sv, \"Free Internet with the SBB FreeSurf app\");\n  expect_notice(\"fr\"sv, \"Connexion Internet gratuite avec l'app FreeSurf CFF\");\n  expect_notice(\"it\"sv, \"Connessione Internet gratuita con l'app FreeSurf FFS\");\n\n  auto const sched_dep = std::chrono::time_point_cast<std::chrono::seconds>(\n      nigiri::parse_time(\"2024-12-15T05:50:00+01:00\", \"%FT%T%Ez\"));\n  auto const sched_arr = std::chrono::time_point_cast<std::chrono::seconds>(\n      nigiri::parse_time(\"2024-12-15T05:57:00+01:00\", \"%FT%T%Ez\"));\n  auto const check_leg =\n      [&](api::Leg const& leg, std::chrono::sys_seconds const dep,\n          std::chrono::sys_seconds const arr, bool const is_rt) {\n        EXPECT_EQ(dep, *leg.startTime_);\n        EXPECT_EQ(arr, *leg.endTime_);\n        EXPECT_EQ(sched_dep, *leg.scheduledStartTime_);\n        EXPECT_EQ(sched_arr, *leg.scheduledEndTime_);\n        EXPECT_EQ(is_rt, leg.realTime_);\n      };\n\n  auto const base_res = trip_ep(base_trip);\n  ASSERT_EQ(1, base_res.legs_.size());\n  check_leg(base_res.legs_.front(), sched_dep, sched_arr, false);\n\n  {\n    constexpr auto kNetexSiriUpdate = R\"(<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Siri xmlns=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n  <ServiceDelivery>\n    <ResponseTimestamp>2024-12-15T05:40:00</ResponseTimestamp>\n    <EstimatedTimetableDelivery version=\"2.0\">\n      <ResponseTimestamp>2024-12-15T05:40:00</ResponseTimestamp>\n      <EstimatedJourneyVersionFrame>\n        <RecordedAtTime>2024-12-15T05:40:00</RecordedAtTime>\n        <EstimatedVehicleJourney>\n          <LineRef>LineDoesNotMatter</LineRef>\n          <DirectionRef>Up</DirectionRef>\n          <FramedVehicleJourneyRef>\n            <DataFrameRef>2024-12-15</DataFrameRef>\n            <DatedVehicleJourneyRef>unknown</DatedVehicleJourneyRef>\n          </FramedVehicleJourneyRef>\n          <RecordedCalls>\n            <RecordedCall>\n              <StopPointRef>ch:1:sloid:30243:0:403158</StopPointRef>\n              <AimedDepartureTime>2024-12-15T05:50:00+01:00</AimedDepartureTime>\n              <ExpectedDepartureTime>2024-12-15T05:52:00+01:00</ExpectedDepartureTime>\n            </RecordedCall>\n          </RecordedCalls>\n          <EstimatedCalls>\n            <EstimatedCall>\n              <StopPointRef>ch:1:sloid:1954:0:845083</StopPointRef>\n              <AimedArrivalTime>2024-12-15T05:57:00+01:00</AimedArrivalTime>\n              <ExpectedArrivalTime>2024-12-15T05:59:00+01:00</ExpectedArrivalTime>\n            </EstimatedCall>\n          </EstimatedCalls>\n        </EstimatedVehicleJourney>\n      </EstimatedJourneyVersionFrame>\n    </EstimatedTimetableDelivery>\n  </ServiceDelivery>\n</Siri>\n)\";\n\n    auto siri_updater = auser(*d.tt_, d.tags_->get_src(\"netex\"),\n                              nigiri::rt::vdv_aus::updater::xml_format::kSiri);\n    auto const expected_siri_state =\n        nigiri::parse_time_no_tz(\"2024-12-15T05:40:00\")\n            .time_since_epoch()\n            .count();\n    auto const siri_stats = siri_updater.consume_update(kNetexSiriUpdate, rtt);\n    EXPECT_EQ(1U, siri_stats.matched_runs_);\n    EXPECT_EQ(2U, siri_stats.updated_events_);\n    EXPECT_EQ(expected_siri_state, siri_updater.update_state_);\n    auto const siri_dep = std::chrono::time_point_cast<std::chrono::seconds>(\n        nigiri::parse_time(\"2024-12-15T05:52:00+01:00\", \"%FT%T%Ez\"));\n    auto const siri_arr = std::chrono::time_point_cast<std::chrono::seconds>(\n        nigiri::parse_time(\"2024-12-15T05:59:00+01:00\", \"%FT%T%Ez\"));\n    auto const siri_res = trip_ep(base_trip);\n    ASSERT_EQ(1, siri_res.legs_.size());\n    check_leg(siri_res.legs_.front(), siri_dep, siri_arr, true);\n  }\n\n  {\n    constexpr auto kNetexVdvUpdate =\n        R\"(<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n<DatenAbrufenAntwort>\n  <Bestaetigung Zst=\"2024-12-15T05:40:00\" Ergebnis=\"ok\" Fehlernummer=\"0\" />\n  <AUSNachricht AboID=\"1\" auser_id=\"314159\">\n    <IstFahrt Zst=\"2024-12-15T05:50:00\">\n      <LinienID>NET</LinienID>\n      <RichtungsID>1</RichtungsID>\n      <FahrtRef>\n        <FahrtID>\n          <FahrtBezeichner>NETEX</FahrtBezeichner>\n          <Betriebstag>2024-12-15</Betriebstag>\n        </FahrtID>\n      </FahrtRef>\n      <Komplettfahrt>true</Komplettfahrt>\n      <BetreiberID>MOTIS</BetreiberID>\n      <IstHalt>\n        <HaltID>ch:1:sloid:30243:0:403158</HaltID>\n        <Abfahrtszeit>2024-12-15T04:50:00</Abfahrtszeit>\n        <IstAbfahrtPrognose>2024-12-15T04:56:00</IstAbfahrtPrognose>\n      </IstHalt>\n      <IstHalt>\n        <HaltID>ch:1:sloid:1954:0:845083</HaltID>\n        <Ankunftszeit>2024-12-15T04:57:00</Ankunftszeit>\n        <IstAnkunftPrognose>2024-12-15T05:03:00</IstAnkunftPrognose>\n      </IstHalt>\n      <LinienText>NET</LinienText>\n      <ProduktID>NET</ProduktID>\n      <RichtungsText>Netex Demo</RichtungsText>\n      <Zusatzfahrt>false</Zusatzfahrt>\n      <FaelltAus>false</FaelltAus>\n    </IstFahrt>\n  </AUSNachricht>\n</DatenAbrufenAntwort>\n)\";\n\n    auto vdv_updater = auser(*d.tt_, d.tags_->get_src(\"netex\"),\n                             nigiri::rt::vdv_aus::updater::xml_format::kVdv);\n    auto const vdv_stats = vdv_updater.consume_update(kNetexVdvUpdate, rtt);\n    EXPECT_EQ(1U, vdv_stats.matched_runs_);\n    EXPECT_EQ(2U, vdv_stats.updated_events_);\n    EXPECT_EQ(314159, vdv_updater.update_state_);\n    auto const vdv_dep = std::chrono::time_point_cast<std::chrono::seconds>(\n        nigiri::parse_time(\"2024-12-15T04:56:00\", \"%FT%T\"));\n    auto const vdv_arr = std::chrono::time_point_cast<std::chrono::seconds>(\n        nigiri::parse_time(\"2024-12-15T05:03:00\", \"%FT%T\"));\n    auto const vdv_res = trip_ep(base_trip);\n    ASSERT_EQ(1, vdv_res.legs_.size());\n    check_leg(vdv_res.legs_.front(), vdv_dep, vdv_arr, true);\n  }\n}\n"
  },
  {
    "path": "test/flex_mode_id_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/flex/mode_id.h\"\n\nusing namespace motis::flex;\n\nTEST(motis, flex_mode_id_zero) {\n  auto const t = nigiri::flex_transport_idx_t{0U};\n  auto const stop = 0U;\n  auto const dir = osr::direction::kForward;\n  auto const id = mode_id{t, stop, dir}.to_id();\n  EXPECT_TRUE(mode_id::is_flex(id));\n\n  auto const id1 = mode_id{id};\n  EXPECT_EQ(stop, id1.get_stop());\n  EXPECT_EQ(dir, id1.get_dir());\n  EXPECT_EQ(t, id1.get_flex_transport());\n}\n\nTEST(motis, flex_mode_id) {\n  auto const t = nigiri::flex_transport_idx_t{44444U};\n  auto const stop = 15;\n  auto const dir = osr::direction::kBackward;\n  auto const id = mode_id{t, stop, dir}.to_id();\n  EXPECT_TRUE(mode_id::is_flex(id));\n\n  auto const id1 = mode_id{id};\n  EXPECT_EQ(stop, id1.get_stop());\n  EXPECT_EQ(dir, id1.get_dir());\n  EXPECT_EQ(t, id1.get_flex_transport());\n}"
  },
  {
    "path": "test/gbfs_partition_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <algorithm>\n#include <array>\n#include <set>\n#include <vector>\n\n#include \"motis/gbfs/partition.h\"\n\nusing namespace motis::gbfs;\n\n// helper function to compare sets regardless of order\ntemplate <typename T>\nbool compare_partitions(std::vector<std::vector<T>> const& actual,\n                        std::vector<std::vector<T>> const& expected) {\n  if (actual.size() != expected.size()) {\n    return false;\n  }\n\n  auto actual_sets = std::vector<std::set<T>>{};\n  auto expected_sets = std::vector<std::set<T>>{};\n\n  for (auto const& vec : actual) {\n    actual_sets.emplace_back(begin(vec), end(vec));\n  }\n  for (auto const& vec : expected) {\n    expected_sets.emplace_back(begin(vec), end(vec));\n  }\n\n  std::sort(actual_sets.begin(), actual_sets.end());\n  std::sort(expected_sets.begin(), expected_sets.end());\n\n  return actual_sets == expected_sets;\n}\n\nTEST(motis, gbfs_partition_test) {\n  using T = int;\n\n  auto p = partition<T>{10};\n\n  // Initial partition test\n  auto const single_set =\n      std::vector<std::vector<T>>{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}};\n  EXPECT_TRUE(compare_partitions(p.get_sets(), single_set));\n\n  // First refinement\n  p.refine(std::array{1, 2, 3});\n  auto const two_sets =\n      std::vector<std::vector<T>>{{1, 2, 3}, {0, 4, 5, 6, 7, 8, 9}};\n  EXPECT_TRUE(compare_partitions(p.get_sets(), two_sets));\n\n  // Same refinement in different orders should yield same result\n  p.refine(std::array{1, 2, 3});\n  EXPECT_TRUE(compare_partitions(p.get_sets(), two_sets));\n  p.refine(std::array{3, 2, 1});\n  EXPECT_TRUE(compare_partitions(p.get_sets(), two_sets));\n\n  // Further refinement\n  p.refine(std::array{7, 8});\n  auto const three_sets =\n      std::vector<std::vector<T>>{{1, 2, 3}, {7, 8}, {0, 4, 5, 6, 9}};\n  EXPECT_TRUE(compare_partitions(p.get_sets(), three_sets));\n\n  // Final refinement\n  p.refine(std::array{1, 3});\n  auto const four_sets =\n      std::vector<std::vector<T>>{{1, 3}, {2}, {7, 8}, {0, 4, 5, 6, 9}};\n  EXPECT_TRUE(compare_partitions(p.get_sets(), four_sets));\n}\n\nTEST(motis, gbfs_partition_empty_refinement) {\n  using T = int;\n  auto p = partition<T>{5};\n\n  p.refine(std::array<T, 0>{});\n  EXPECT_TRUE(compare_partitions(p.get_sets(),\n                                 std::vector<std::vector<T>>{{0, 1, 2, 3, 4}}));\n}\n\nTEST(motis, gbfs_partition_single_element) {\n  using T = int;\n  auto p = partition<T>{1};\n\n  p.refine(std::array{0});\n  EXPECT_TRUE(\n      compare_partitions(p.get_sets(), std::vector<std::vector<T>>{{0}}));\n}\n\nTEST(motis, gbfs_partition_disjoint_refinements) {\n  using T = int;\n  auto p = partition<T>{6};\n\n  p.refine(std::array{0, 1});\n  p.refine(std::array{2, 3});\n  p.refine(std::array{4, 5});\n\n  auto const expected = std::vector<std::vector<T>>{{0, 1}, {2, 3}, {4, 5}};\n  EXPECT_TRUE(compare_partitions(p.get_sets(), expected));\n}\n"
  },
  {
    "path": "test/main.cc",
    "content": "#include <filesystem>\n\n#include \"google/protobuf/arena.h\"\n#include \"gtest/gtest.h\"\n\n#include \"utl/progress_tracker.h\"\n\n#include \"test_dir.h\"\n\n#ifdef PROTOBUF_LINKED\n#include \"google/protobuf/stubs/common.h\"\n#endif\n\nnamespace fs = std::filesystem;\n\nint main(int argc, char** argv) {\n  std::clog.rdbuf(std::cout.rdbuf());\n\n  auto const progress_tracker = utl::activate_progress_tracker(\"test\");\n  auto const silencer = utl::global_progress_bars{true};\n  fs::current_path(OSR_TEST_EXECUTION_DIR);\n\n  ::testing::InitGoogleTest(&argc, argv);\n  auto test_result = RUN_ALL_TESTS();\n\n  google::protobuf::ShutdownProtobufLibrary();\n\n  return test_result;\n}"
  },
  {
    "path": "test/matching_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"osr/platforms.h\"\n#include \"osr/routing/profile.h\"\n#include \"osr/routing/profiles/bike.h\"\n#include \"osr/routing/profiles/bike_sharing.h\"\n#include \"osr/routing/profiles/car.h\"\n#include \"osr/routing/profiles/car_sharing.h\"\n#include \"osr/routing/profiles/foot.h\"\n#include \"osr/types.h\"\n\n#include \"nigiri/timetable.h\"\n#include \"nigiri/types.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/import.h\"\n#include \"motis/match_platforms.h\"\n#include \"motis/osr/parameters.h\"\n\nusing namespace std::string_view_literals;\nusing namespace osr;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3,DB,S3,,,109\nU4,DB,U4,,,402\nICE,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3,S1,S3,,\nU4,S1,U4,,\nICE,S1,ICE,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nS3,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0\nU4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nU4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0\nICE,00:35:00,00:35:00,DA_10,0,0,0\nICE,00:45:00,00:45:00,FFM_10,1,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n\n# frequencies.txt\ntrip_id,start_time,end_time,headway_secs\nS3,01:15:00,25:15:00,3600\nICE,00:35:00,24:35:00,3600\nU4,01:05:00,25:01:00,3600\n)\"sv;\n\nTEST(motis, get_track) {\n  ASSERT_FALSE(motis::get_track(\"a:\").has_value());\n\n  auto const track = motis::get_track(\"a:232\");\n  ASSERT_TRUE(track.has_value());\n  EXPECT_EQ(\"232\", *track);\n\n  auto const track_1 = motis::get_track(\"232\");\n  ASSERT_TRUE(track_1.has_value());\n  EXPECT_EQ(\"232\", *track_1);\n}\n\nTEST(motis, get_way_candidates) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = motis::config{\n      .server_ = {{.web_folder_ = \"ui/build\", .n_threads_ = 1U}},\n      .osm_ = {\"test/resources/test_case.osm.pbf\"},\n      .timetable_ =\n          motis::config::timetable{\n              .first_day_ = \"2019-05-01\",\n              .num_days_ = 2,\n              .use_osm_stop_coordinates_ = true,\n              .extend_missing_footpaths_ = false,\n              .preprocess_max_matching_distance_ = 250,\n              .datasets_ = {{\"test\", {.path_ = std::string{kGTFS}}}}},\n      .street_routing_ = true,\n      .osr_footpath_ = true,\n      .geocoding_ = true,\n      .reverse_geocoding_ = true};\n  motis::import(c, \"test/data\");\n  auto d = motis::data{\"test/data\", c};\n  auto const location_idxs =\n      utl::to_vec(utl::enumerate(d.tt_->locations_.src_),\n                  [&](std::tuple<size_t, nigiri::source_idx_t> const ll) {\n                    return nigiri::location_idx_t{std::get<0>(ll)};\n                  });\n  auto const locs =\n      utl::to_vec(location_idxs, [&](nigiri::location_idx_t const l) {\n        return osr::location{\n            d.tt_->locations_.coordinates_[nigiri::location_idx_t{l}],\n            d.pl_->get_level(*d.w_, (*d.matches_)[nigiri::location_idx_t{l}])};\n      });\n\n  auto const get_path = [&](search_profile const p, way_candidate const& a,\n                            node_candidate const& anc, location const& l) {\n    switch (p) {\n      case search_profile::kFoot: [[fallthrough]];\n      case search_profile::kWheelchair: [[fallthrough]];\n      case search_profile::kCar: [[fallthrough]];\n      case search_profile::kBike: [[fallthrough]];\n      case search_profile::kCarSharing: [[fallthrough]];\n      case search_profile::kBikeSharing:\n        return d.l_->get_node_candidate_path(a, anc, true, l);\n      default: return std::vector<geo::latlng>{};\n    }\n  };\n\n  for (auto profile :\n       {osr::search_profile::kCar, osr::search_profile::kCarSharing,\n        osr::search_profile::kFoot, osr::search_profile::kWheelchair,\n        osr::search_profile::kBike, osr::search_profile::kBikeSharing}) {\n    auto const with_preprocessing = motis::get_reverse_platform_way_matches(\n        *d.l_, &*d.way_matches_, profile, location_idxs, locs,\n        osr::direction::kForward, 250);\n    auto const without_preprocessing = utl::to_vec(\n        utl::zip(location_idxs, locs),\n        [&](std::tuple<nigiri::location_idx_t, osr::location> const ll) {\n          auto const& [l, query] = ll;\n          return d.l_->match(motis::to_profile_parameters(profile, {}), query,\n                             true, osr::direction::kForward, 250, nullptr,\n                             profile);\n        });\n\n    ASSERT_EQ(with_preprocessing.size(), without_preprocessing.size());\n    for (auto const [with, without, l] :\n         utl::zip(with_preprocessing, without_preprocessing, locs)) {\n      ASSERT_EQ(with.size(), without.size());\n      auto sorted_with = with;\n      auto sorted_without = without;\n      auto const sort_by_way = [&](auto const& a, auto const& b) {\n        return a.way_ < b.way_;\n      };\n      utl::sort(sorted_with, sort_by_way);\n      utl::sort(sorted_without, sort_by_way);\n      for (auto [a, b] : utl::zip(sorted_with, sorted_without)) {\n        ASSERT_FLOAT_EQ(a.dist_to_way_, b.dist_to_way_);\n        ASSERT_EQ(a.way_, b.way_);\n        for (auto const& [anc, bnc] :\n             {std::tuple{a.left_, b.left_}, std::tuple{a.right_, b.right_}}) {\n          ASSERT_EQ(anc.node_, bnc.node_);\n          if (anc.valid()) {\n            EXPECT_FLOAT_EQ(anc.dist_to_node_, bnc.dist_to_node_);\n            EXPECT_EQ(anc.cost_, bnc.cost_);\n            EXPECT_EQ(anc.lvl_, bnc.lvl_);\n            EXPECT_EQ(anc.way_dir_, bnc.way_dir_);\n            EXPECT_EQ(get_path(profile, a, anc, l), bnc.path_);\n          }\n        }\n      }\n    }\n\n    auto const with_preprocessing_but_larger =\n        motis::get_reverse_platform_way_matches(*d.l_, &*d.way_matches_,\n                                                profile, location_idxs, locs,\n                                                osr::direction::kForward, 500);\n\n    auto const with_preprocessing_but_smaller =\n        motis::get_reverse_platform_way_matches(*d.l_, &*d.way_matches_,\n                                                profile, location_idxs, locs,\n                                                osr::direction::kForward, 50);\n\n    for (auto const [with, larger, smaller] :\n         utl::zip(with_preprocessing, with_preprocessing_but_larger,\n                  with_preprocessing_but_smaller)) {\n      if (with.size() == 0 && larger.size() == 0 && smaller.size() == 0) {\n        continue;\n      }\n      EXPECT_GE(larger.size(), with.size());\n      EXPECT_GE(with.size(), smaller.size());\n      auto const& a = larger[0];\n      auto const& b = smaller[0];\n      EXPECT_TRUE(!a.left_.valid() ||\n                  a.left_.path_.size() != 0);  // on the fly match\n      EXPECT_TRUE(!b.left_.valid() ||\n                  b.left_.path_.size() == 0);  // preprocessed match\n    }\n\n    for (auto dist : {5, 10, 25, 250, 1000}) {\n      auto const remote_station =\n          osr::location{{49.8731904, 8.6221451}, level_t{}};\n      auto const raw = d.l_->get_raw_match(remote_station, dist);\n      auto const params = motis::to_profile_parameters(profile, {});\n      auto const with =\n          d.l_->match(params, remote_station, true, osr::direction::kForward,\n                      dist, nullptr, profile, raw);\n      auto const without =\n          d.l_->match(params, remote_station, true, osr::direction::kForward,\n                      dist, nullptr, profile);\n      EXPECT_NE(0, raw.size());\n      EXPECT_EQ(with.size(), without.size());\n    }\n  }\n}\n"
  },
  {
    "path": "test/odm/csv_journey_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/odm/journeys.h\"\n#include \"motis/odm/odm.h\"\n\nusing namespace std::string_view_literals;\nusing namespace date;\nusing namespace std::chrono_literals;\nusing namespace motis::odm;\n\nconstexpr auto const csv0 =\n    R\"__(departure, arrival, transfers, first_mile_mode, first_mile_duration, last_mile_mode, last_mile_duration\n2025-06-16 12:34, 2025-06-16 23:45, 3, taxi, 12, walk, 04\n2025-06-17 11:11, 2025-06-17 22:22, 2, walk, 05, walk, 06\n2025-06-18 06:33, 2025-06-18 07:44, 8, taxi, 20, taxi, 20\n)__\"sv;\n\nTEST(odm, csv_journeys_in_out) { EXPECT_EQ(csv0, to_csv(from_csv(csv0))); }\n\nconstexpr auto const direct_odm_csv =\n    R\"__(departure, arrival, transfers, first_mile_mode, first_mile_duration, last_mile_mode, last_mile_duration\n2025-06-16 12:00, 2025-06-16 13:00, 0, taxi, 60, walk, 00\n)__\"sv;\n\nTEST(odm, csv_journeys_direct) {\n  EXPECT_EQ(\n      direct_odm_csv,\n      to_csv(std::vector<nigiri::routing::journey>{make_odm_direct(\n          nigiri::location_idx_t::invalid(), nigiri::location_idx_t::invalid(),\n          nigiri::unixtime_t{date::sys_days{2025_y / June / 16} + 12h},\n          nigiri::unixtime_t{date::sys_days{2025_y / June / 16} + 13h})}));\n}"
  },
  {
    "path": "test/odm/prima_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"nigiri/loader/dir.h\"\n#include \"nigiri/loader/gtfs/load_timetable.h\"\n#include \"nigiri/loader/init_finish.h\"\n#include \"nigiri/common/parse_time.h\"\n#include \"nigiri/routing/journey.h\"\n#include \"nigiri/special_stations.h\"\n\n#include \"motis/odm/odm.h\"\n#include \"motis/odm/prima.h\"\n#include \"motis/transport_mode_ids.h\"\n\n#include \"motis-api/motis-api.h\"\n\nnamespace n = nigiri;\nnamespace nr = nigiri::routing;\nusing namespace motis::odm;\nusing namespace std::chrono_literals;\nusing namespace date;\n\nn::loader::mem_dir tt_files() {\n  return n::loader::mem_dir::read(R\"__(\n\"(\n# stops.txt\nstop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station\nA,A,A,0.1,0.1,,,,\nB,B,B,0.2,0.2,,,,\nC,C,C,0.3,0.3,,,,\nD,D,D,0.4,0.4,,,,\n)__\");\n}\n\nconstexpr auto blacklist_request =\n    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}})\";\n\nconstexpr auto invalid_response = R\"({\"message\":\"Internal Error\"})\";\n\nconstexpr auto blacklist_response = R\"(\n{\n  \"start\": [[{\"startTime\": 32400000, \"endTime\": 43200000}],[{\"startTime\": 43200000, \"endTime\": 64800000}]],\n  \"target\": [[{\"startTime\": 43200000, \"endTime\": 64800000}],[]],\n  \"direct\": [{\"startTime\": 43200000, \"endTime\": 64800000}]\n}\n)\";\n\n// 1970-01-01T09:57:00Z, 1970-01-01T10:55:00Z\n// 1970-01-01T14:07:00Z, 1970-01-01T14:46:00Z\n// 1970-01-01T11:30:00Z, 1970-01-01T12:30:00Z\nconstexpr auto whitelisting_response = R\"(\n{\n  \"start\": [[{\"pickupTime\": 35820000, \"dropoffTime\": 39300000}],[null]],\n  \"target\": [[{\"pickupTime\": 50820000, \"dropoffTime\": 53160000}]],\n  \"direct\": [{\"pickupTime\": 41400000,\"dropoffTime\": 45000000}]\n}\n)\";\n\nconstexpr auto adjusted_to_whitelisting = R\"(\n[1970-01-01 09:57, 1970-01-01 12:00]\nTRANSFERS: 0\n     FROM: (START, START) [1970-01-01 09:57]\n       TO: (END, END) [1970-01-01 12:00]\nleg 0: (START, START) [1970-01-01 09:57] -> (A, A) [1970-01-01 10:55]\n  MUMO (id=16, duration=58)\nleg 1: (A, A) [1970-01-01 10:55] -> (A, A) [1970-01-01 11:00]\n  FOOTPATH (duration=5)\nleg 2: (A, A) [1970-01-01 11:00] -> (END, END) [1970-01-01 12:00]\n  MUMO (id=0, duration=60)\n\n[1970-01-01 09:57, 1970-01-01 14:46]\nTRANSFERS: 0\n     FROM: (START, START) [1970-01-01 09:57]\n       TO: (END, END) [1970-01-01 14:46]\nleg 0: (START, START) [1970-01-01 09:57] -> (A, A) [1970-01-01 10:55]\n  MUMO (id=16, duration=58)\nleg 1: (A, A) [1970-01-01 10:55] -> (A, A) [1970-01-01 11:00]\n  FOOTPATH (duration=5)\nleg 2: (A, A) [1970-01-01 11:00] -> (C, C) [1970-01-01 13:00]\n  MUMO (id=1000000, duration=120)\nleg 3: (C, C) [1970-01-01 13:00] -> (C, C) [1970-01-01 14:07]\n  FOOTPATH (duration=67)\nleg 4: (C, C) [1970-01-01 14:07] -> (END, END) [1970-01-01 14:46]\n  MUMO (id=16, duration=39)\n\n)\";\n\nTEST(odm, prima_update) {\n  n::timetable tt;\n  tt.date_range_ = {date::sys_days{2017_y / January / 1},\n                    date::sys_days{2017_y / January / 2}};\n  n::loader::register_special_stations(tt);\n  auto const src = n::source_idx_t{0};\n  n::loader::gtfs::load_timetable({.default_tz_ = \"Europe/Berlin\"}, src,\n                                  tt_files(), tt);\n  n::loader::finalize(tt);\n\n  auto const get_loc_idx = [&](auto&& s) {\n    return tt.locations_.location_id_to_idx_.at({.id_ = s, .src_ = src});\n  };\n\n  auto const loc = osr::location{};\n  auto p = prima{\"prima_url\", loc, loc, motis::api::plan_params{}};\n  p.fixed_ = n::event_type::kDep;\n  p.cap_ = {.wheelchairs_ = 1, .bikes_ = 0, .passengers_ = 1, .luggage_ = 0};\n  p.first_mile_taxi_ = {\n      {get_loc_idx(\"A\"), n::duration_t{60min}, motis::kOdmTransportModeId},\n      {get_loc_idx(\"B\"), n::duration_t{60min}, motis::kOdmTransportModeId}};\n  p.last_mile_taxi_ = {\n      {get_loc_idx(\"C\"), n::duration_t{60min}, motis::kOdmTransportModeId},\n      {get_loc_idx(\"D\"), n::duration_t{60min}, motis::kOdmTransportModeId}};\n\n  EXPECT_EQ(p.make_blacklist_taxi_request(\n                tt, {n::unixtime_t{0h}, n::unixtime_t{48h}}),\n            blacklist_request);\n\n  EXPECT_FALSE(p.consume_blacklist_taxi_response(invalid_response));\n  EXPECT_TRUE(p.consume_blacklist_taxi_response(blacklist_response));\n\n  ASSERT_EQ(p.first_mile_taxi_.size(), 2);\n  EXPECT_EQ(p.first_mile_taxi_[0].target_, get_loc_idx(\"A\"));\n  ASSERT_EQ(p.first_mile_taxi_times_[0].size(), 1);\n  EXPECT_EQ(p.first_mile_taxi_times_[0][0].from_, to_unix(32400000));\n  EXPECT_EQ(p.first_mile_taxi_times_[0][0].to_, to_unix(43200000));\n  EXPECT_EQ(p.first_mile_taxi_[1].target_, get_loc_idx(\"B\"));\n  ASSERT_EQ(p.first_mile_taxi_times_[1].size(), 1);\n  EXPECT_EQ(p.first_mile_taxi_times_[1][0].from_, to_unix(43200000));\n  EXPECT_EQ(p.first_mile_taxi_times_[1][0].to_, to_unix(64800000));\n\n  ASSERT_EQ(p.last_mile_taxi_.size(), 2);\n  EXPECT_EQ(p.last_mile_taxi_[0].target_, get_loc_idx(\"C\"));\n  ASSERT_EQ(p.last_mile_taxi_times_[0].size(), 1);\n  EXPECT_EQ(p.last_mile_taxi_times_[0][0].from_, to_unix(43200000));\n  EXPECT_EQ(p.last_mile_taxi_times_[0][0].to_, to_unix(64800000));\n  EXPECT_EQ(p.last_mile_taxi_[1].target_, get_loc_idx(\"D\"));\n  EXPECT_EQ(p.last_mile_taxi_times_[1].size(), 0);\n\n  auto const expected_direct_interval =\n      n::interval{to_unix(43200000), to_unix(64800000)};\n  for (auto const& d : p.direct_taxi_) {\n    EXPECT_TRUE(expected_direct_interval.contains(d.dep_));\n  }\n\n  auto taxi_journeys = std::vector<nr::journey>{};\n  taxi_journeys.push_back(\n      {.legs_ = {{n::direction::kForward,\n                  n::get_special_station(n::special_station::kStart),\n                  get_loc_idx(\"A\"), n::unixtime_t{10h}, n::unixtime_t{11h},\n                  nr::offset{get_loc_idx(\"A\"), 1h, motis::kOdmTransportModeId}},\n                 {n::direction::kForward, get_loc_idx(\"A\"),\n                  n::get_special_station(n::special_station::kEnd),\n                  n::unixtime_t{11h}, n::unixtime_t{12h},\n                  nr::offset{get_loc_idx(\"A\"), 1h, kWalkTransportModeId}}},\n       .start_time_ = n::unixtime_t{10h},\n       .dest_time_ = n::unixtime_t{12h},\n       .dest_ = n::get_special_station(n::special_station::kEnd)});\n\n  taxi_journeys.push_back(\n      {.legs_ = {{n::direction::kForward,\n                  n::get_special_station(n::special_station::kStart),\n                  get_loc_idx(\"B\"), n::unixtime_t{11h}, n::unixtime_t{12h},\n                  nr::offset{get_loc_idx(\"B\"), 1h, motis::kOdmTransportModeId}},\n                 {n::direction::kForward, get_loc_idx(\"B\"),\n                  n::get_special_station(n::special_station::kEnd),\n                  n::unixtime_t{12h}, n::unixtime_t{13h},\n                  nr::offset{get_loc_idx(\"B\"), 1h, kWalkTransportModeId}}},\n       .start_time_ = n::unixtime_t{11h},\n       .dest_time_ = n::unixtime_t{13h},\n       .dest_ = n::get_special_station(n::special_station::kEnd)});\n\n  taxi_journeys.push_back(\n      {.legs_ = {{n::direction::kForward,\n                  n::get_special_station(n::special_station::kStart),\n                  get_loc_idx(\"A\"), n::unixtime_t{10h}, n::unixtime_t{11h},\n                  n::routing::offset{get_loc_idx(\"A\"), 1h,\n                                     motis::kOdmTransportModeId}},\n                 {n::direction::kForward, get_loc_idx(\"A\"), get_loc_idx(\"C\"),\n                  n::unixtime_t{11h}, n::unixtime_t{13h},\n                  nr::offset{get_loc_idx(\"C\"), 2h, motis::kFlexModeIdOffset}},\n                 {n::direction::kForward, get_loc_idx(\"C\"),\n                  n::get_special_station(n::special_station::kEnd),\n                  n::unixtime_t{13h}, n::unixtime_t{14h},\n                  nr::offset{get_loc_idx(\"C\"), 1h,\n                             motis::kOdmTransportModeId}}},\n       .start_time_ = n::unixtime_t{10h},\n       .dest_time_ = n::unixtime_t{14h},\n       .dest_ = n::get_special_station(n::special_station::kEnd)});\n\n  p.direct_taxi_ = {\n      direct_ride{.dep_ = n::unixtime_t{11h}, .arr_ = n::unixtime_t{12h}}};\n\n  auto first_mile_taxi_rides = std::vector<nr::start>{};\n  auto last_mile_taxi_rides = std::vector<nr::start>{};\n  extract_taxis(taxi_journeys, first_mile_taxi_rides, last_mile_taxi_rides);\n  EXPECT_FALSE(p.consume_whitelist_taxi_response(\n      invalid_response, taxi_journeys, first_mile_taxi_rides,\n      last_mile_taxi_rides));\n  EXPECT_TRUE(p.consume_whitelist_taxi_response(\n      whitelisting_response, taxi_journeys, first_mile_taxi_rides,\n      last_mile_taxi_rides));\n\n  auto ss = std::stringstream{};\n  ss << \"\\n\";\n  for (auto const& j : taxi_journeys) {\n    j.print(ss, tt, nullptr);\n    ss << \"\\n\";\n  }\n\n  EXPECT_EQ(adjusted_to_whitelisting, ss.str());\n}\n"
  },
  {
    "path": "test/odm/td_offsets_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/odm/td_offsets.h\"\n#include \"motis/transport_mode_ids.h\"\n\nusing namespace nigiri;\nusing namespace nigiri::routing;\nusing namespace std::chrono_literals;\n\nnamespace motis::odm {\n\nvoid print(td_offsets_t const& tdos) {\n  for (auto const& [l, tdo] : tdos) {\n    std::cout << \"l: \" << l << \":\\n\";\n    for (auto const& t : tdo) {\n      std::cout << \"[valid_from_: \" << t.valid_from_\n                << \", duration_: \" << t.duration_\n                << \", transport_mode_id_: \" << t.transport_mode_id_ << \"]\\n\";\n    }\n  }\n}\n\nTEST(odm, get_td_offsets_basic) {\n  auto const rides = std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                                         .time_at_stop_ = unixtime_t{11h},\n                                         .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 1min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_extension) {\n  auto const rides =\n      std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                          .time_at_stop_ = unixtime_t{11h},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 1min},\n                          .time_at_stop_ = unixtime_t{11h + 1min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{11h + 2min},\n                          .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 3min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_extension_reverse) {\n  auto const rides =\n      std::vector<start>{{.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{11h + 2min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 1min},\n                          .time_at_stop_ = unixtime_t{11h + 1min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h},\n                          .time_at_stop_ = unixtime_t{11h},\n                          .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 3min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_extension_fill_gap) {\n  auto const rides =\n      std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                          .time_at_stop_ = unixtime_t{11h},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{11h + 2min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 1min},\n                          .time_at_stop_ = unixtime_t{11h + 1min},\n                          .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 3min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_intermittent) {\n  auto const rides = std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                                         .time_at_stop_ = unixtime_t{11h},\n                                         .stop_ = location_idx_t{1U}},\n                                        {.time_at_start_ = unixtime_t{11h},\n                                         .time_at_stop_ = unixtime_t{12h},\n                                         .stop_ = location_idx_t{1U}},\n                                        {.time_at_start_ = unixtime_t{12h},\n                                         .time_at_stop_ = unixtime_t{13h},\n                                         .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 6U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 1min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].valid_from_, unixtime_t{11h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].transport_mode_id_,\n            kOdmTransportModeId);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].valid_from_,\n            unixtime_t{11h + 1min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[4].valid_from_, unixtime_t{12h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[4].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[4].transport_mode_id_,\n            kOdmTransportModeId);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[5].valid_from_,\n            unixtime_t{12h + 1min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[5].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[5].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_long_short_long) {\n  auto const rides =\n      std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                          .time_at_stop_ = unixtime_t{11h},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{11h + 2min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 1min},\n                          .time_at_stop_ = unixtime_t{10h + 31min},\n                          .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 4U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 1min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, 30min);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].valid_from_,\n            unixtime_t{10h + 2min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].valid_from_,\n            unixtime_t{10h + 3min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_late_improvement) {\n  auto const rides =\n      std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                          .time_at_stop_ = unixtime_t{11h},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 1min},\n                          .time_at_stop_ = unixtime_t{11h + 1min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{11h + 2min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 3min},\n                          .time_at_stop_ = unixtime_t{11h + 3min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{10h + 32min},\n                          .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 4U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 2min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_, 30min);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].valid_from_,\n            unixtime_t{10h + 3min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[2].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].valid_from_,\n            unixtime_t{10h + 4min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[3].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\nTEST(odm, get_td_offsets_late_worse) {\n  auto const rides =\n      std::vector<start>{{.time_at_start_ = unixtime_t{10h},\n                          .time_at_stop_ = unixtime_t{11h},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 1min},\n                          .time_at_stop_ = unixtime_t{11h + 1min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{11h + 2min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 3min},\n                          .time_at_stop_ = unixtime_t{11h + 3min},\n                          .stop_ = location_idx_t{1U}},\n                         {.time_at_start_ = unixtime_t{10h + 2min},\n                          .time_at_stop_ = unixtime_t{12h + 2min},\n                          .stop_ = location_idx_t{1U}}};\n\n  auto const td_offsets =\n      motis::odm::get_td_offsets(rides, kOdmTransportModeId);\n\n  print(td_offsets);\n\n  ASSERT_TRUE(td_offsets.contains(location_idx_t{1U}));\n  ASSERT_EQ(td_offsets.at(location_idx_t{1U}).size(), 2U);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].valid_from_, unixtime_t{10h});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].duration_, 1h);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[0].transport_mode_id_,\n            kOdmTransportModeId);\n\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].valid_from_,\n            unixtime_t{10h + 4min});\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].duration_,\n            footpath::kMaxDuration);\n  EXPECT_EQ(td_offsets.at(location_idx_t{1U})[1].transport_mode_id_,\n            kOdmTransportModeId);\n}\n\n}  // namespace motis::odm"
  },
  {
    "path": "test/read_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/parse_location.h\"\n\nusing namespace motis;\nusing namespace date;\nnamespace n = nigiri;\n\nusing namespace std::chrono_literals;\n\nTEST(motis, parse_location_with_level) {\n  auto const parsed = parse_location(\"-123.1,44.2,-1.5\");\n  ASSERT_TRUE(parsed.has_value());\n  EXPECT_EQ((osr::location{{-123.1, 44.2}, osr::level_t{-1.5F}}), *parsed);\n}\n\nTEST(motis, parse_location_no_level) {\n  auto const parsed = parse_location(\"-23.1,45.2\");\n  ASSERT_TRUE(parsed.has_value());\n  EXPECT_EQ((osr::location{{-23.1, 45.2}, osr::kNoLevel}), *parsed);\n}\n\nTEST(motis, parse_cursor_earlier) {\n  auto const q = cursor_to_query(\"EARLIER|1720036560\");\n\n  ASSERT_TRUE(\n      std::holds_alternative<n::interval<n::unixtime_t>>(q.start_time_));\n\n  auto const interval = std::get<n::interval<n::unixtime_t>>(q.start_time_);\n  EXPECT_EQ(sys_days{2024_y / July / 3} + 17h + 56min, interval.from_);\n  EXPECT_EQ(sys_days{2024_y / July / 3} + 19h + 56min, interval.to_);\n}\n\nTEST(motis, parse_cursor_later) {\n  auto const q = cursor_to_query(\"LATER|1720036560\");\n\n  ASSERT_TRUE(\n      std::holds_alternative<n::interval<n::unixtime_t>>(q.start_time_));\n\n  auto const interval = std::get<n::interval<n::unixtime_t>>(q.start_time_);\n  EXPECT_EQ(sys_days{2024_y / July / 3} + 19h + 56min, interval.from_);\n  EXPECT_EQ(sys_days{2024_y / July / 3} + 21h + 56min, interval.to_);\n}"
  },
  {
    "path": "test/resources/gbfs/free_bike_status.json",
    "content": "{\n  \"last_updated\": 1729500733,\n  \"ttl\": 240,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"bikes\": [\n      {\n        \"bike_id\": \"CAB:Vehicle:887f506f-23fb-48d4-92f7-3c74446e729c\",\n        \"lat\": 49.87530897258006,\n        \"lon\": 8.627667300924486,\n        \"is_reserved\": false,\n        \"is_disabled\": false,\n        \"rental_uris\": {\n          \"android\": \"https://www.callabike.de/bike?number=12404\",\n          \"ios\": \"https://www.callabike.de/bike?number=12404\"\n        },\n        \"vehicle_type_id\": \"CAB:VehicleType:832e0956-f155-3c82-9211-c2beb9f6929d\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "test/resources/gbfs/gbfs.json",
    "content": "{\n  \"last_updated\": 1728356899,\n  \"ttl\": 86400,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"de\": {\n      \"feeds\": [\n        {\n          \"name\": \"gbfs\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/gbfs\"\n        },\n        {\n          \"name\": \"system_information\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/system_information\"\n        },\n        {\n          \"name\": \"vehicle_types\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/vehicle_types\"\n        },\n        {\n          \"name\": \"station_information\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/station_information\"\n        },\n        {\n          \"name\": \"station_status\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/station_status\"\n        },\n        {\n          \"name\": \"free_bike_status\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/free_bike_status\"\n        },\n        {\n          \"name\": \"geofencing_zones\",\n          \"url\": \"https://api.mobidata-bw.de/sharing/gbfs/v2/callabike/geofencing_zones\"\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "test/resources/gbfs/geofencing_zones.json",
    "content": "{\n  \"last_updated\": 1729479604,\n  \"ttl\": 86400,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"geofencing_zones\": {\n      \"type\": \"FeatureCollection\",\n      \"features\": []\n    }\n  }\n}"
  },
  {
    "path": "test/resources/gbfs/station_information.json",
    "content": "{\n  \"last_updated\": 1729479276,\n  \"ttl\": 86400,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"stations\": [\n      {\n        \"station_id\": \"CAB:Station:4a58211d-7fc2-4897-bc9f-3767f804953a\",\n        \"name\": \"Darmstadt Hbf\",\n        \"lat\": 49.871651,\n        \"lon\": 8.631084\n      }\n    ]\n  }\n}"
  },
  {
    "path": "test/resources/gbfs/station_status.json",
    "content": "{\n  \"last_updated\": 1729500733,\n  \"ttl\": 240,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"stations\": [\n      {\n        \"station_id\": \"CAB:Station:4a58211d-7fc2-4897-bc9f-3767f804953a\",\n        \"num_bikes_available\": 48,\n        \"vehicle_types_available\": [\n          {\n            \"vehicle_type_id\": \"CAB:VehicleType:832e0956-f155-3c82-9211-c2beb9f6929d\",\n            \"count\": 48\n          }\n        ],\n        \"is_installed\": true,\n        \"is_renting\": true,\n        \"is_returning\": true,\n        \"last_reported\": 1729500733\n      }\n    ]\n  }\n}"
  },
  {
    "path": "test/resources/gbfs/system_information.json",
    "content": "{\n  \"last_updated\": 1728960694,\n  \"ttl\": 86400,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"system_id\": \"callabike\",\n    \"language\": \"de\",\n    \"name\": \"Call a Bike\",\n    \"short_name\": \"CaB\",\n    \"operator\": \"Deutsche Bahn Connect GmbH\",\n    \"url\": \"https://www.callabike.de\",\n    \"feed_contact_email\": \"doServices.Sirius.Team@deutschebahn.com\",\n    \"timezone\": \"Europe/Berlin\",\n    \"rental_apps\": {\n      \"android\": {\n        \"store_uri\": \"https://play.google.com/store/apps/details?id=de.bahn.callabike\",\n        \"discovery_uri\": \"callabike://\"\n      },\n      \"ios\": {\n        \"store_uri\": \"https://apps.apple.com/de/app/call-a-bike/id420360589\",\n        \"discovery_uri\": \"callabike://\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "test/resources/gbfs/vehicle_types.json",
    "content": "{\n  \"last_updated\": 1729497561,\n  \"ttl\": 86400,\n  \"version\": \"2.3\",\n  \"data\": {\n    \"vehicle_types\": [\n      {\n        \"vehicle_type_id\": \"CAB:VehicleType:832e0956-f155-3c82-9211-c2beb9f6929d\",\n        \"form_factor\": \"bicycle\",\n        \"propulsion_type\": \"human\",\n        \"name\": \"bike\",\n        \"return_constraint\": \"hybrid\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "test/resources/ojp/geocoding_request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n<OJPRequest>\n  <siri:ServiceRequest>\n    <siri:ServiceRequestContext>\n      <siri:Language>de</siri:Language>\n    </siri:ServiceRequestContext>\n    <siri:RequestTimestamp>2026-01-28T07:22:00.862Z</siri:RequestTimestamp>\n    <siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>\n    <OJPLocationInformationRequest>\n      <siri:RequestTimestamp>2026-01-28T07:22:00.862Z</siri:RequestTimestamp>\n      <InitialInput>\n        <Name>Darmstadt Hauptbahnhof</Name>\n      </InitialInput>\n      <Restrictions>\n        <Type>stop</Type>\n        <NumberOfResults>10</NumberOfResults>\n        <IncludePtModes>true</IncludePtModes>\n      </Restrictions>\n    </OJPLocationInformationRequest>\n  </siri:ServiceRequest>\n</OJPRequest>\n\n</OJP>\n"
  },
  {
    "path": "test/resources/ojp/geocoding_response.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns:siri=\"http://www.siri.org.uk/siri\" xmlns=\"http://www.vdv.de/ojp\" version=\"2.0\">\n  <OJPResponse>\n    <siri:ServiceDelivery>\n      <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n      <siri:ProducerRef>MOTIS</siri:ProducerRef>\n      <siri:ResponseMessageIdentifier>MSG</siri:ResponseMessageIdentifier>\n      <OJPLocationInformationDelivery>\n        <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n        <siri:DefaultLanguage>de</siri:DefaultLanguage>\n      </OJPLocationInformationDelivery>\n    </siri:ServiceDelivery>\n  </OJPResponse>\n</OJP>"
  },
  {
    "path": "test/resources/ojp/intermodal_routing_request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n<OJPRequest>\n  <siri:ServiceRequest>\n    <siri:ServiceRequestContext>\n      <siri:Language>de</siri:Language>\n    </siri:ServiceRequestContext>\n    <siri:RequestTimestamp>2019-05-01T01:15Z</siri:RequestTimestamp>\n    <siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>\n    <OJPTripRequest>\n      <siri:RequestTimestamp>2019-05-01T01:15Z</siri:RequestTimestamp>\n      <Origin>\n        <PlaceRef>\n          <GeoPosition>\n            <siri:Longitude>8.6586978</siri:Longitude>\n            <siri:Latitude>50.1040763</siri:Latitude>\n            <Properties />\n          </GeoPosition>\n          <Name>\n            <Text>n/a</Text>\n          </Name>\n        </PlaceRef>\n        <DepArrTime>2019-05-01T01:15Z</DepArrTime>\n      </Origin>\n      <Destination>\n        <PlaceRef>\n          <GeoPosition>\n            <siri:Longitude>8.6767235</siri:Longitude>\n            <siri:Latitude>50.1132737</siri:Latitude>\n            <Properties />\n          </GeoPosition>\n          <Name>\n            <Text>n/a</Text>\n          </Name>\n        </PlaceRef>\n      </Destination>\n      <Params>\n        <NumberOfResults>2</NumberOfResults>\n        <IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>\n        <IncludeTrackSections>true</IncludeTrackSections>\n        <IncludeLegProjection>true</IncludeLegProjection>\n        <IncludeIntermediateStops>true</IncludeIntermediateStops>\n      </Params>\n    </OJPTripRequest>\n  </siri:ServiceRequest>\n</OJPRequest>\n\n</OJP>\n"
  },
  {
    "path": "test/resources/ojp/intermodal_routing_response.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns:siri=\"http://www.siri.org.uk/siri\" xmlns=\"http://www.vdv.de/ojp\" version=\"2.0\">\n  <OJPResponse>\n    <siri:ServiceDelivery>\n      <siri:ResponseTimestamp>2026-02-16T11:08:06.083Z</siri:ResponseTimestamp>\n      <siri:ProducerRef>MOTIS</siri:ProducerRef>\n      <siri:ResponseMessageIdentifier>6</siri:ResponseMessageIdentifier>\n      <OJPTripDelivery>\n        <siri:ResponseTimestamp>2026-02-16T11:08:06.083Z</siri:ResponseTimestamp>\n        <siri:DefaultLanguage>de</siri:DefaultLanguage>\n        <TripResponseContext>\n          <Places>\n            <Place>\n              <StopPlace>\n                <siri:StopPlaceRef>test_FFM</siri:StopPlaceRef>\n                <StopPlaceName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPlaceName>\n              </StopPlace>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66341</siri:Longitude>\n                <siri:Latitude>50.10701</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_de:6412:10:6:1</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPointName>\n                <ParentRef>test_FFM</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66382</siri:Longitude>\n                <siri:Latitude>50.10758</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPlace>\n                <siri:StopPlaceRef>test_FFM_HAUPT</siri:StopPlaceRef>\n                <StopPlaceName>\n                  <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                </StopPlaceName>\n              </StopPlace>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hauptwache</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.67835</siri:Longitude>\n                <siri:Latitude>50.11403</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_FFM_HAUPT_U</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                </StopPointName>\n                <ParentRef>test_FFM_HAUPT</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hauptwache</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.67912</siri:Longitude>\n                <siri:Latitude>50.11385</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_FFM_101</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPointName>\n                <ParentRef>test_FFM</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66333</siri:Longitude>\n                <siri:Latitude>50.10739</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_FFM_HAUPT_S</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                </StopPointName>\n                <ParentRef>test_FFM_HAUPT</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hauptwache</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.67824</siri:Longitude>\n                <siri:Latitude>50.11404</siri:Latitude>\n              </GeoPosition>\n            </Place>\n          </Places>\n        </TripResponseContext>\n        <TripResult>\n          <Id>1</Id>\n          <Trip>\n            <Id>1</Id>\n            <Duration>PT19M</Duration>\n            <StartTime>2019-05-01T01:55:00Z</StartTime>\n            <EndTime>2019-05-01T02:14:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>102</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <ContinuousLeg>\n                <LegStart>\n                  <GeoPosition>\n                    <siri:Longitude>8.6587</siri:Longitude>\n                    <siri:Latitude>50.10408</siri:Latitude>\n                  </GeoPosition>\n                  <Name>\n                    <Text xml:lang=\"de\">START</Text>\n                  </Name>\n                </LegStart>\n                <LegEnd>\n                  <siri:StopPointRef>test_de:6412:10:6:1</siri:StopPointRef>\n                  <Name>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </Name>\n                </LegEnd>\n                <Service>\n                  <PersonalModeOfOperation>own</PersonalModeOfOperation>\n                  <PersonalMode>foot</PersonalMode>\n                </Service>\n                <Duration>PT10M</Duration>\n                <Length>57</Length>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <GeoPosition>\n                        <siri:Longitude>8.6587</siri:Longitude>\n                        <siri:Latitude>50.10408</siri:Latitude>\n                      </GeoPosition>\n                      <Name>\n                        <Text xml:lang=\"de\">START</Text>\n                      </Name>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <siri:StopPointRef>test_de:6412:10:6:1</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection>\n                      <Position>\n                        <siri:Longitude>8.65869</siri:Longitude>\n                        <siri:Latitude>50.10409</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65907</siri:Longitude>\n                        <siri:Latitude>50.1042</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65932</siri:Longitude>\n                        <siri:Latitude>50.10428</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65932</siri:Longitude>\n                        <siri:Latitude>50.10428</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65999</siri:Longitude>\n                        <siri:Latitude>50.10452</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66032</siri:Longitude>\n                        <siri:Latitude>50.10465</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66119</siri:Longitude>\n                        <siri:Latitude>50.10499</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66189</siri:Longitude>\n                        <siri:Latitude>50.10527</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6626</siri:Longitude>\n                        <siri:Latitude>50.10554</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66337</siri:Longitude>\n                        <siri:Latitude>50.10584</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10601</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10601</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66386</siri:Longitude>\n                        <siri:Latitude>50.10604</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66386</siri:Longitude>\n                        <siri:Latitude>50.10604</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10611</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10611</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66374</siri:Longitude>\n                        <siri:Latitude>50.10616</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66374</siri:Longitude>\n                        <siri:Latitude>50.10616</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66372</siri:Longitude>\n                        <siri:Latitude>50.10618</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66372</siri:Longitude>\n                        <siri:Latitude>50.10618</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66368</siri:Longitude>\n                        <siri:Latitude>50.10622</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66368</siri:Longitude>\n                        <siri:Latitude>50.10622</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66365</siri:Longitude>\n                        <siri:Latitude>50.10626</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66365</siri:Longitude>\n                        <siri:Latitude>50.10626</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66384</siri:Longitude>\n                        <siri:Latitude>50.10633</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66384</siri:Longitude>\n                        <siri:Latitude>50.10633</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6637</siri:Longitude>\n                        <siri:Latitude>50.10648</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6637</siri:Longitude>\n                        <siri:Latitude>50.10648</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66353</siri:Longitude>\n                        <siri:Latitude>50.10666</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66353</siri:Longitude>\n                        <siri:Latitude>50.10666</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66345</siri:Longitude>\n                        <siri:Latitude>50.10674</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66345</siri:Longitude>\n                        <siri:Latitude>50.10674</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66332</siri:Longitude>\n                        <siri:Latitude>50.10688</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66332</siri:Longitude>\n                        <siri:Latitude>50.10688</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66375</siri:Longitude>\n                        <siri:Latitude>50.10705</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66375</siri:Longitude>\n                        <siri:Latitude>50.10705</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66378</siri:Longitude>\n                        <siri:Latitude>50.10706</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66378</siri:Longitude>\n                        <siri:Latitude>50.10706</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66372</siri:Longitude>\n                        <siri:Latitude>50.10714</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66372</siri:Longitude>\n                        <siri:Latitude>50.10714</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66375</siri:Longitude>\n                        <siri:Latitude>50.10713</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66375</siri:Longitude>\n                        <siri:Latitude>50.10713</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66389</siri:Longitude>\n                        <siri:Latitude>50.10719</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66389</siri:Longitude>\n                        <siri:Latitude>50.10719</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66397</siri:Longitude>\n                        <siri:Latitude>50.10724</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66397</siri:Longitude>\n                        <siri:Latitude>50.10724</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66406</siri:Longitude>\n                        <siri:Latitude>50.10727</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66406</siri:Longitude>\n                        <siri:Latitude>50.10727</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6641</siri:Longitude>\n                        <siri:Latitude>50.10729</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6641</siri:Longitude>\n                        <siri:Latitude>50.10729</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66405</siri:Longitude>\n                        <siri:Latitude>50.10731</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66405</siri:Longitude>\n                        <siri:Latitude>50.10731</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66399</siri:Longitude>\n                        <siri:Latitude>50.10737</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66399</siri:Longitude>\n                        <siri:Latitude>50.10737</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66397</siri:Longitude>\n                        <siri:Latitude>50.10736</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6639</siri:Longitude>\n                        <siri:Latitude>50.10744</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66377</siri:Longitude>\n                        <siri:Latitude>50.10756</siri:Latitude>\n                      </Position>\n                    </LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>57</Length>\n                  </TrackSection>\n                </LegTrack>\n              </ContinuousLeg>\n            </Leg>\n            <Leg>\n              <Id>2</Id>\n              <Duration>PT5M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_de:6412:10:6:1</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">U4</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T02:05:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T02:05:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_HAUPT_U</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                  </StopPointName>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T02:10:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T02:10:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_04:05_test_U4</JourneyRef>\n                  <LineRef>test_U4</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">U4</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>metro</PtMode>\n                    <siri:MetroSubmode>tube</siri:MetroSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_de:6412:10:6:1</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_HAUPT_U</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection>\n                      <Position>\n                        <siri:Longitude>8.66382</siri:Longitude>\n                        <siri:Latitude>50.10758</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67912</siri:Longitude>\n                        <siri:Latitude>50.11385</siri:Latitude>\n                      </Position>\n                    </LinkProjection>\n                    <Duration>PT5M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n            <Leg>\n              <Id>3</Id>\n              <Duration>PT4M</Duration>\n              <ContinuousLeg>\n                <LegStart>\n                  <siri:StopPointRef>test_FFM_HAUPT_U</siri:StopPointRef>\n                  <Name>\n                    <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                  </Name>\n                </LegStart>\n                <LegEnd>\n                  <GeoPosition>\n                    <siri:Longitude>8.67672</siri:Longitude>\n                    <siri:Latitude>50.11327</siri:Latitude>\n                  </GeoPosition>\n                  <Name>\n                    <Text xml:lang=\"de\">END</Text>\n                  </Name>\n                </LegEnd>\n                <Service>\n                  <PersonalModeOfOperation>own</PersonalModeOfOperation>\n                  <PersonalMode>foot</PersonalMode>\n                </Service>\n                <Duration>PT4M</Duration>\n                <Length>43</Length>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_FFM_HAUPT_U</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                      </Name>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <GeoPosition>\n                        <siri:Longitude>8.67672</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </GeoPosition>\n                      <Name>\n                        <Text xml:lang=\"de\">END</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection>\n                      <Position>\n                        <siri:Longitude>8.67918</siri:Longitude>\n                        <siri:Latitude>50.11384</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67917</siri:Longitude>\n                        <siri:Latitude>50.1138</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67913</siri:Longitude>\n                        <siri:Latitude>50.11376</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67913</siri:Longitude>\n                        <siri:Latitude>50.11376</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6791</siri:Longitude>\n                        <siri:Latitude>50.11376</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6791</siri:Longitude>\n                        <siri:Latitude>50.11376</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67897</siri:Longitude>\n                        <siri:Latitude>50.11347</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67884</siri:Longitude>\n                        <siri:Latitude>50.11329</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67885</siri:Longitude>\n                        <siri:Latitude>50.11329</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67885</siri:Longitude>\n                        <siri:Latitude>50.11329</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6788</siri:Longitude>\n                        <siri:Latitude>50.11322</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6788</siri:Longitude>\n                        <siri:Latitude>50.11322</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67876</siri:Longitude>\n                        <siri:Latitude>50.11315</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67876</siri:Longitude>\n                        <siri:Latitude>50.11315</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67864</siri:Longitude>\n                        <siri:Latitude>50.11321</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67864</siri:Longitude>\n                        <siri:Latitude>50.11321</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67828</siri:Longitude>\n                        <siri:Latitude>50.11337</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67828</siri:Longitude>\n                        <siri:Latitude>50.11337</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67812</siri:Longitude>\n                        <siri:Latitude>50.1134</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67812</siri:Longitude>\n                        <siri:Latitude>50.1134</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67796</siri:Longitude>\n                        <siri:Latitude>50.11342</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67796</siri:Longitude>\n                        <siri:Latitude>50.11342</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67792</siri:Longitude>\n                        <siri:Latitude>50.11342</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67792</siri:Longitude>\n                        <siri:Latitude>50.11342</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67793</siri:Longitude>\n                        <siri:Latitude>50.11346</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67793</siri:Longitude>\n                        <siri:Latitude>50.11346</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67756</siri:Longitude>\n                        <siri:Latitude>50.11352</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67756</siri:Longitude>\n                        <siri:Latitude>50.11352</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67706</siri:Longitude>\n                        <siri:Latitude>50.1136</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67706</siri:Longitude>\n                        <siri:Latitude>50.1136</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67696</siri:Longitude>\n                        <siri:Latitude>50.11361</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67696</siri:Longitude>\n                        <siri:Latitude>50.11361</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67693</siri:Longitude>\n                        <siri:Latitude>50.11333</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67693</siri:Longitude>\n                        <siri:Latitude>50.11333</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67683</siri:Longitude>\n                        <siri:Latitude>50.11334</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67682</siri:Longitude>\n                        <siri:Latitude>50.11332</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67682</siri:Longitude>\n                        <siri:Latitude>50.11332</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6768</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6768</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67676</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67673</siri:Longitude>\n                        <siri:Latitude>50.11328</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67673</siri:Longitude>\n                        <siri:Latitude>50.11328</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67673</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </Position>\n                    </LinkProjection>\n                    <Duration>PT4M</Duration>\n                    <Length>43</Length>\n                  </TrackSection>\n                </LegTrack>\n              </ContinuousLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n        <TripResult>\n          <Id>2</Id>\n          <Trip>\n            <Id>2</Id>\n            <Duration>PT19M</Duration>\n            <StartTime>2019-05-01T02:05:00Z</StartTime>\n            <EndTime>2019-05-01T02:24:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>110</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <ContinuousLeg>\n                <LegStart>\n                  <GeoPosition>\n                    <siri:Longitude>8.6587</siri:Longitude>\n                    <siri:Latitude>50.10408</siri:Latitude>\n                  </GeoPosition>\n                  <Name>\n                    <Text xml:lang=\"de\">START</Text>\n                  </Name>\n                </LegStart>\n                <LegEnd>\n                  <siri:StopPointRef>test_FFM_101</siri:StopPointRef>\n                  <Name>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </Name>\n                </LegEnd>\n                <Service>\n                  <PersonalModeOfOperation>own</PersonalModeOfOperation>\n                  <PersonalMode>foot</PersonalMode>\n                </Service>\n                <Duration>PT10M</Duration>\n                <Length>80</Length>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <GeoPosition>\n                        <siri:Longitude>8.6587</siri:Longitude>\n                        <siri:Latitude>50.10408</siri:Latitude>\n                      </GeoPosition>\n                      <Name>\n                        <Text xml:lang=\"de\">START</Text>\n                      </Name>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <siri:StopPointRef>test_FFM_101</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection>\n                      <Position>\n                        <siri:Longitude>8.65869</siri:Longitude>\n                        <siri:Latitude>50.10409</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65907</siri:Longitude>\n                        <siri:Latitude>50.1042</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65932</siri:Longitude>\n                        <siri:Latitude>50.10428</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65932</siri:Longitude>\n                        <siri:Latitude>50.10428</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.65999</siri:Longitude>\n                        <siri:Latitude>50.10452</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66032</siri:Longitude>\n                        <siri:Latitude>50.10465</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66119</siri:Longitude>\n                        <siri:Latitude>50.10499</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66189</siri:Longitude>\n                        <siri:Latitude>50.10527</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6626</siri:Longitude>\n                        <siri:Latitude>50.10554</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66337</siri:Longitude>\n                        <siri:Latitude>50.10584</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10601</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10601</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66386</siri:Longitude>\n                        <siri:Latitude>50.10604</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66386</siri:Longitude>\n                        <siri:Latitude>50.10604</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10611</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66379</siri:Longitude>\n                        <siri:Latitude>50.10611</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66374</siri:Longitude>\n                        <siri:Latitude>50.10616</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66374</siri:Longitude>\n                        <siri:Latitude>50.10616</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66372</siri:Longitude>\n                        <siri:Latitude>50.10618</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66372</siri:Longitude>\n                        <siri:Latitude>50.10618</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66368</siri:Longitude>\n                        <siri:Latitude>50.10622</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66368</siri:Longitude>\n                        <siri:Latitude>50.10622</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66365</siri:Longitude>\n                        <siri:Latitude>50.10626</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66365</siri:Longitude>\n                        <siri:Latitude>50.10626</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66364</siri:Longitude>\n                        <siri:Latitude>50.10627</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66364</siri:Longitude>\n                        <siri:Latitude>50.10627</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66357</siri:Longitude>\n                        <siri:Latitude>50.10634</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66357</siri:Longitude>\n                        <siri:Latitude>50.10634</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66352</siri:Longitude>\n                        <siri:Latitude>50.10639</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66352</siri:Longitude>\n                        <siri:Latitude>50.10639</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66351</siri:Longitude>\n                        <siri:Latitude>50.10641</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66351</siri:Longitude>\n                        <siri:Latitude>50.10641</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66345</siri:Longitude>\n                        <siri:Latitude>50.10646</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66345</siri:Longitude>\n                        <siri:Latitude>50.10646</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6634</siri:Longitude>\n                        <siri:Latitude>50.10652</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6634</siri:Longitude>\n                        <siri:Latitude>50.10652</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66333</siri:Longitude>\n                        <siri:Latitude>50.1066</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66333</siri:Longitude>\n                        <siri:Latitude>50.1066</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66326</siri:Longitude>\n                        <siri:Latitude>50.10667</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66326</siri:Longitude>\n                        <siri:Latitude>50.10667</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66325</siri:Longitude>\n                        <siri:Latitude>50.10668</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66325</siri:Longitude>\n                        <siri:Latitude>50.10668</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66318</siri:Longitude>\n                        <siri:Latitude>50.10676</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66318</siri:Longitude>\n                        <siri:Latitude>50.10676</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6631</siri:Longitude>\n                        <siri:Latitude>50.10683</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6631</siri:Longitude>\n                        <siri:Latitude>50.10683</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66306</siri:Longitude>\n                        <siri:Latitude>50.10687</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66306</siri:Longitude>\n                        <siri:Latitude>50.10687</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66304</siri:Longitude>\n                        <siri:Latitude>50.1069</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66304</siri:Longitude>\n                        <siri:Latitude>50.1069</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66301</siri:Longitude>\n                        <siri:Latitude>50.10693</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66297</siri:Longitude>\n                        <siri:Latitude>50.10697</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66297</siri:Longitude>\n                        <siri:Latitude>50.10697</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6629</siri:Longitude>\n                        <siri:Latitude>50.10704</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6629</siri:Longitude>\n                        <siri:Latitude>50.10704</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66285</siri:Longitude>\n                        <siri:Latitude>50.1071</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66285</siri:Longitude>\n                        <siri:Latitude>50.1071</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66293</siri:Longitude>\n                        <siri:Latitude>50.10714</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66293</siri:Longitude>\n                        <siri:Latitude>50.10714</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6629</siri:Longitude>\n                        <siri:Latitude>50.10714</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6629</siri:Longitude>\n                        <siri:Latitude>50.10714</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66283</siri:Longitude>\n                        <siri:Latitude>50.10722</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6628</siri:Longitude>\n                        <siri:Latitude>50.10721</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6628</siri:Longitude>\n                        <siri:Latitude>50.10721</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66278</siri:Longitude>\n                        <siri:Latitude>50.1072</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66268</siri:Longitude>\n                        <siri:Latitude>50.10731</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66268</siri:Longitude>\n                        <siri:Latitude>50.10731</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66262</siri:Longitude>\n                        <siri:Latitude>50.10729</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66262</siri:Longitude>\n                        <siri:Latitude>50.10729</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66257</siri:Longitude>\n                        <siri:Latitude>50.10715</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66257</siri:Longitude>\n                        <siri:Latitude>50.10715</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66255</siri:Longitude>\n                        <siri:Latitude>50.10712</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66255</siri:Longitude>\n                        <siri:Latitude>50.10712</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66241</siri:Longitude>\n                        <siri:Latitude>50.10706</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66241</siri:Longitude>\n                        <siri:Latitude>50.10706</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66244</siri:Longitude>\n                        <siri:Latitude>50.10703</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66244</siri:Longitude>\n                        <siri:Latitude>50.10703</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66297</siri:Longitude>\n                        <siri:Latitude>50.10724</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66297</siri:Longitude>\n                        <siri:Latitude>50.10724</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.66333</siri:Longitude>\n                        <siri:Latitude>50.10739</siri:Latitude>\n                      </Position>\n                    </LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>80</Length>\n                  </TrackSection>\n                </LegTrack>\n              </ContinuousLeg>\n            </Leg>\n            <Leg>\n              <Id>2</Id>\n              <Duration>PT5M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_FFM_101</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">101</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T02:15:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T02:15:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_HAUPT_S</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                  </StopPointName>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T02:20:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T02:20:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_04:15_test_S3</JourneyRef>\n                  <LineRef>test_S3</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">S3</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>rail</PtMode>\n                    <siri:RailSubmode>suburbanRailway</siri:RailSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_FFM_101</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_HAUPT_S</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection>\n                      <Position>\n                        <siri:Longitude>8.66333</siri:Longitude>\n                        <siri:Latitude>50.10739</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67824</siri:Longitude>\n                        <siri:Latitude>50.11404</siri:Latitude>\n                      </Position>\n                    </LinkProjection>\n                    <Duration>PT5M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n            <Leg>\n              <Id>3</Id>\n              <Duration>PT4M</Duration>\n              <ContinuousLeg>\n                <LegStart>\n                  <siri:StopPointRef>test_FFM_HAUPT_S</siri:StopPointRef>\n                  <Name>\n                    <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                  </Name>\n                </LegStart>\n                <LegEnd>\n                  <GeoPosition>\n                    <siri:Longitude>8.67672</siri:Longitude>\n                    <siri:Latitude>50.11327</siri:Latitude>\n                  </GeoPosition>\n                  <Name>\n                    <Text xml:lang=\"de\">END</Text>\n                  </Name>\n                </LegEnd>\n                <Service>\n                  <PersonalModeOfOperation>own</PersonalModeOfOperation>\n                  <PersonalMode>foot</PersonalMode>\n                </Service>\n                <Duration>PT4M</Duration>\n                <Length>28</Length>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_FFM_HAUPT_S</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hauptwache</Text>\n                      </Name>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <GeoPosition>\n                        <siri:Longitude>8.67672</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </GeoPosition>\n                      <Name>\n                        <Text xml:lang=\"de\">END</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection>\n                      <Position>\n                        <siri:Longitude>8.67824</siri:Longitude>\n                        <siri:Latitude>50.11404</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67823</siri:Longitude>\n                        <siri:Latitude>50.11404</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67823</siri:Longitude>\n                        <siri:Latitude>50.11404</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67819</siri:Longitude>\n                        <siri:Latitude>50.11404</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67819</siri:Longitude>\n                        <siri:Latitude>50.11404</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6782</siri:Longitude>\n                        <siri:Latitude>50.11412</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.6782</siri:Longitude>\n                        <siri:Latitude>50.11412</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67762</siri:Longitude>\n                        <siri:Latitude>50.11416</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67707</siri:Longitude>\n                        <siri:Latitude>50.11415</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67671</siri:Longitude>\n                        <siri:Latitude>50.11412</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67671</siri:Longitude>\n                        <siri:Latitude>50.11412</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67669</siri:Longitude>\n                        <siri:Latitude>50.11398</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67669</siri:Longitude>\n                        <siri:Latitude>50.11398</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67668</siri:Longitude>\n                        <siri:Latitude>50.11397</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67665</siri:Longitude>\n                        <siri:Latitude>50.11385</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67665</siri:Longitude>\n                        <siri:Latitude>50.11385</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67662</siri:Longitude>\n                        <siri:Latitude>50.11375</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67662</siri:Longitude>\n                        <siri:Latitude>50.11375</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67661</siri:Longitude>\n                        <siri:Latitude>50.11373</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67661</siri:Longitude>\n                        <siri:Latitude>50.11373</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67658</siri:Longitude>\n                        <siri:Latitude>50.11364</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67658</siri:Longitude>\n                        <siri:Latitude>50.11364</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67672</siri:Longitude>\n                        <siri:Latitude>50.11363</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67679</siri:Longitude>\n                        <siri:Latitude>50.11359</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67679</siri:Longitude>\n                        <siri:Latitude>50.11351</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67673</siri:Longitude>\n                        <siri:Latitude>50.11328</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67673</siri:Longitude>\n                        <siri:Latitude>50.11328</siri:Latitude>\n                      </Position>\n                      <Position>\n                        <siri:Longitude>8.67673</siri:Longitude>\n                        <siri:Latitude>50.11327</siri:Latitude>\n                      </Position>\n                    </LinkProjection>\n                    <Duration>PT4M</Duration>\n                    <Length>28</Length>\n                  </TrackSection>\n                </LegTrack>\n              </ContinuousLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n      </OJPTripDelivery>\n    </siri:ServiceDelivery>\n  </OJPResponse>\n</OJP>"
  },
  {
    "path": "test/resources/ojp/map_stops_request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n<OJPRequest>\n  <siri:ServiceRequest>\n    <siri:ServiceRequestContext>\n      <siri:Language>de</siri:Language>\n    </siri:ServiceRequestContext>\n    <siri:RequestTimestamp>2026-01-28T07:47:11.377Z</siri:RequestTimestamp>\n    <siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>\n    <OJPLocationInformationRequest>\n      <siri:RequestTimestamp>2026-01-28T07:47:11.377Z</siri:RequestTimestamp>\n      <InitialInput>\n        <GeoRestriction>\n          <Rectangle>\n            <UpperLeft>\n              <siri:Latitude>49.87400</siri:Latitude>\n              <siri:Longitude>8.62850</siri:Longitude>\n            </UpperLeft>\n            <LowerRight>\n              <siri:Latitude>49.87100</siri:Latitude>\n              <siri:Longitude>8.63250</siri:Longitude>\n            </LowerRight>\n          </Rectangle>\n        </GeoRestriction>\n      </InitialInput>\n      <Restrictions>\n        <Type>stop</Type>\n        <NumberOfResults>300</NumberOfResults>\n        <IncludePtModes>true</IncludePtModes>\n      </Restrictions>\n    </OJPLocationInformationRequest>\n  </siri:ServiceRequest>\n</OJPRequest>\n\n</OJP>\n"
  },
  {
    "path": "test/resources/ojp/map_stops_response.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns:siri=\"http://www.siri.org.uk/siri\" xmlns=\"http://www.vdv.de/ojp\" version=\"2.0\">\n  <OJPResponse>\n    <siri:ServiceDelivery>\n      <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n      <siri:ProducerRef>MOTIS</siri:ProducerRef>\n      <siri:ResponseMessageIdentifier>MSG</siri:ResponseMessageIdentifier>\n      <OJPLocationInformationDelivery>\n        <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n        <siri:DefaultLanguage>de</siri:DefaultLanguage>\n        <PlaceResult>\n          <Place>\n            <StopPlace>\n              <StopPlaceRef>test_DA</StopPlaceRef>\n              <StopPlaceName>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </StopPlaceName>\n            </StopPlace>\n            <Name>\n              <Text xml:lang=\"de\">DA Hbf</Text>\n            </Name>\n            <GeoPosition>\n              <siri:Longitude>8.63085</siri:Longitude>\n              <siri:Latitude>49.8726</siri:Latitude>\n            </GeoPosition>\n            <Mode>\n              <PtMode>rail</PtMode>\n              <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n            </Mode>\n          </Place>\n          <Complete>true</Complete>\n          <Probability>1</Probability>\n        </PlaceResult>\n        <PlaceResult>\n          <Place>\n            <StopPlace>\n              <StopPlaceRef>test_DA_3</StopPlaceRef>\n              <StopPlaceName>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </StopPlaceName>\n            </StopPlace>\n            <Name>\n              <Text xml:lang=\"de\">DA Hbf</Text>\n            </Name>\n            <GeoPosition>\n              <siri:Longitude>8.63003</siri:Longitude>\n              <siri:Latitude>49.87355</siri:Latitude>\n            </GeoPosition>\n            <Mode>\n              <PtMode>rail</PtMode>\n              <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n            </Mode>\n          </Place>\n          <Complete>true</Complete>\n          <Probability>1</Probability>\n        </PlaceResult>\n        <PlaceResult>\n          <Place>\n            <StopPlace>\n              <StopPlaceRef>test_DA_10</StopPlaceRef>\n              <StopPlaceName>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </StopPlaceName>\n            </StopPlace>\n            <Name>\n              <Text xml:lang=\"de\">DA Hbf</Text>\n            </Name>\n            <GeoPosition>\n              <siri:Longitude>8.62926</siri:Longitude>\n              <siri:Latitude>49.87336</siri:Latitude>\n            </GeoPosition>\n            <Mode>\n              <PtMode>rail</PtMode>\n              <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n            </Mode>\n          </Place>\n          <Complete>true</Complete>\n          <Probability>1</Probability>\n        </PlaceResult>\n      </OJPLocationInformationDelivery>\n    </siri:ServiceDelivery>\n  </OJPResponse>\n</OJP>"
  },
  {
    "path": "test/resources/ojp/routing_request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n<OJPRequest>\n  <siri:ServiceRequest>\n    <siri:ServiceRequestContext>\n      <siri:Language>de</siri:Language>\n    </siri:ServiceRequestContext>\n    <siri:RequestTimestamp>2019-05-01T00:30:00Z</siri:RequestTimestamp>\n    <siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>\n    <OJPTripRequest>\n      <siri:RequestTimestamp>2019-05-01T00:30:00Z</siri:RequestTimestamp>\n      <Origin>\n        <PlaceRef>\n          <StopPlaceRef>test_DA</StopPlaceRef>\n          <Name>\n            <Text>n/a</Text>\n          </Name>\n        </PlaceRef>\n        <DepArrTime>2019-05-01T00:30:00Z</DepArrTime>\n        <IndividualTransportOption />\n      </Origin>\n      <Destination>\n        <PlaceRef>\n          <StopPlaceRef>test_FFM</StopPlaceRef>\n          <Name>\n            <Text>n/a</Text>\n          </Name>\n        </PlaceRef>\n        <IndividualTransportOption />\n      </Destination>\n      <Params>\n        <NumberOfResults>5</NumberOfResults>\n        <IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>\n        <IncludeTrackSections>true</IncludeTrackSections>\n        <IncludeLegProjection>true</IncludeLegProjection>\n        <IncludeTurnDescription>true</IncludeTurnDescription>\n        <IncludeIntermediateStops>true</IncludeIntermediateStops>\n        <UseRealtimeData>explanatory</UseRealtimeData>\n      </Params>\n    </OJPTripRequest>\n  </siri:ServiceRequest>\n</OJPRequest>\n</OJP>\n"
  },
  {
    "path": "test/resources/ojp/routing_response.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns:siri=\"http://www.siri.org.uk/siri\" xmlns=\"http://www.vdv.de/ojp\" version=\"2.0\">\n  <OJPResponse>\n    <siri:ServiceDelivery>\n      <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n      <siri:ProducerRef>MOTIS</siri:ProducerRef>\n      <siri:ResponseMessageIdentifier>MSG</siri:ResponseMessageIdentifier>\n      <OJPTripDelivery>\n        <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n        <siri:DefaultLanguage>de</siri:DefaultLanguage>\n        <TripResponseContext>\n          <Places>\n            <Place>\n              <StopPlace>\n                <siri:StopPlaceRef>test_DA</siri:StopPlaceRef>\n                <StopPlaceName>\n                  <Text xml:lang=\"de\">DA Hbf</Text>\n                </StopPlaceName>\n              </StopPlace>\n              <Name>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.63085</siri:Longitude>\n                <siri:Latitude>49.8726</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">DA Hbf</Text>\n                </StopPointName>\n                <ParentRef>test_DA</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.62926</siri:Longitude>\n                <siri:Latitude>49.87336</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPlace>\n                <siri:StopPlaceRef>test_FFM</siri:StopPlaceRef>\n                <StopPlaceName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPlaceName>\n              </StopPlace>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66341</siri:Longitude>\n                <siri:Latitude>50.10701</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPointName>\n                <ParentRef>test_FFM</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66118</siri:Longitude>\n                <siri:Latitude>50.10593</siri:Latitude>\n              </GeoPosition>\n            </Place>\n          </Places>\n        </TripResponseContext>\n        <TripResult>\n          <Id>1</Id>\n          <Trip>\n            <Id>1</Id>\n            <Duration>PT10M</Duration>\n            <StartTime>2019-05-01T00:35:00Z</StartTime>\n            <EndTime>2019-05-01T00:45:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>2</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">DA Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T00:35:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T00:35:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T00:45:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T00:45:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_02:35_test_ICE</JourneyRef>\n                  <LineRef>test_ICE</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">ICE</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>rail</PtMode>\n                    <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">DA Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection></LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n        <TripResult>\n          <Id>2</Id>\n          <Trip>\n            <Id>2</Id>\n            <Duration>PT10M</Duration>\n            <StartTime>2019-05-01T01:35:00Z</StartTime>\n            <EndTime>2019-05-01T01:45:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>2</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">DA Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T01:35:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T01:35:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T01:45:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T01:45:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_03:35_test_ICE</JourneyRef>\n                  <LineRef>test_ICE</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">ICE</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>rail</PtMode>\n                    <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">DA Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection></LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n        <TripResult>\n          <Id>3</Id>\n          <Trip>\n            <Id>3</Id>\n            <Duration>PT10M</Duration>\n            <StartTime>2019-05-01T02:35:00Z</StartTime>\n            <EndTime>2019-05-01T02:45:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>2</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">DA Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T02:35:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T02:35:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T02:45:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T02:45:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_04:35_test_ICE</JourneyRef>\n                  <LineRef>test_ICE</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">ICE</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>rail</PtMode>\n                    <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">DA Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection></LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n        <TripResult>\n          <Id>4</Id>\n          <Trip>\n            <Id>4</Id>\n            <Duration>PT10M</Duration>\n            <StartTime>2019-05-01T03:35:00Z</StartTime>\n            <EndTime>2019-05-01T03:45:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>2</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">DA Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T03:35:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T03:35:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T03:45:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T03:45:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_05:35_test_ICE</JourneyRef>\n                  <LineRef>test_ICE</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">ICE</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>rail</PtMode>\n                    <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">DA Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection></LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n        <TripResult>\n          <Id>5</Id>\n          <Trip>\n            <Id>5</Id>\n            <Duration>PT10M</Duration>\n            <StartTime>2019-05-01T04:35:00Z</StartTime>\n            <EndTime>2019-05-01T04:45:00Z</EndTime>\n            <Transfers>0</Transfers>\n            <Distance>2</Distance>\n            <Leg>\n              <Id>1</Id>\n              <Duration>PT10M</Duration>\n              <TimedLeg>\n                <LegBoard>\n                  <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">DA Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceDeparture>\n                    <TimetabledTime>2019-05-01T04:35:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T04:35:00Z</EstimatedTime>\n                  </ServiceDeparture>\n                  <Order>1</Order>\n                </LegBoard>\n                <LegAlight>\n                  <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                  <StopPointName>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </StopPointName>\n                  <PlannedQuay>\n                    <Text xml:lang=\"de\">10</Text>\n                  </PlannedQuay>\n                  <ServiceArrival>\n                    <TimetabledTime>2019-05-01T04:45:00Z</TimetabledTime>\n                    <EstimatedTime>2019-05-01T04:45:00Z</EstimatedTime>\n                  </ServiceArrival>\n                  <Order>2</Order>\n                </LegAlight>\n                <Service>\n                  <OperatingDayRef>20190501</OperatingDayRef>\n                  <JourneyRef>20190501_06:35_test_ICE</JourneyRef>\n                  <LineRef>test_ICE</LineRef>\n                  <DirectionRef>0</DirectionRef>\n                  <siri:OperatorRef>DB</siri:OperatorRef>\n                  <ProductCategory />\n                  <DestinationText>\n                    <Text xml:lang=\"de\">FFM Hbf</Text>\n                  </DestinationText>\n                  <PublishedServiceName>\n                    <Text xml:lang=\"de\">ICE</Text>\n                  </PublishedServiceName>\n                  <Mode>\n                    <PtMode>rail</PtMode>\n                    <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n                  </Mode>\n                </Service>\n                <LegTrack>\n                  <TrackSection>\n                    <TrackSectionStart>\n                      <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                      <Name>\n                        <Text xml:lang=\"de\">DA Hbf</Text>\n                      </Name>\n                      <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                    </TrackSectionStart>\n                    <TrackSectionEnd>\n                      <Name>\n                        <Text xml:lang=\"de\">FFM Hbf</Text>\n                      </Name>\n                    </TrackSectionEnd>\n                    <LinkProjection></LinkProjection>\n                    <Duration>PT10M</Duration>\n                    <Length>2</Length>\n                  </TrackSection>\n                </LegTrack>\n              </TimedLeg>\n            </Leg>\n          </Trip>\n        </TripResult>\n      </OJPTripDelivery>\n    </siri:ServiceDelivery>\n  </OJPResponse>\n</OJP>"
  },
  {
    "path": "test/resources/ojp/stop_event_request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n<OJPRequest>\n  <siri:ServiceRequest>\n    <siri:ServiceRequestContext>\n      <siri:Language>de</siri:Language>\n    </siri:ServiceRequestContext>\n    <siri:RequestTimestamp>2026-01-28T07:38:53.987Z</siri:RequestTimestamp>\n    <siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>\n    <OJPStopEventRequest>\n      <siri:RequestTimestamp>2026-01-28T07:38:53.987Z</siri:RequestTimestamp>\n      <Location>\n        <PlaceRef>\n          <siri:StopPointRef>test_DA_3</siri:StopPointRef>\n          <Name>\n            <Text>n/a</Text>\n          </Name>\n        </PlaceRef>\n        <DepArrTime>2026-01-28T06:00:00.000Z</DepArrTime>\n      </Location>\n      <Params>\n        <IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>\n        <NumberOfResults>10</NumberOfResults>\n        <StopEventType>departure</StopEventType>\n        <IncludePreviousCalls>true</IncludePreviousCalls>\n        <IncludeOnwardCalls>true</IncludeOnwardCalls>\n        <UseRealtimeData>explanatory</UseRealtimeData>\n      </Params>\n    </OJPStopEventRequest>\n  </siri:ServiceRequest>\n</OJPRequest>\n\n</OJP>\n"
  },
  {
    "path": "test/resources/ojp/stop_event_response.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns:siri=\"http://www.siri.org.uk/siri\" xmlns=\"http://www.vdv.de/ojp\" version=\"2.0\">\n  <OJPResponse>\n    <siri:ServiceDelivery>\n      <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n      <siri:ProducerRef>MOTIS</siri:ProducerRef>\n      <siri:ResponseMessageIdentifier>MSG</siri:ResponseMessageIdentifier>\n      <OJPStopEventDelivery>\n        <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n        <siri:DefaultLanguage>de</siri:DefaultLanguage>\n        <StopEventResponseContext>\n          <Places />\n          <Situations />\n        </StopEventResponseContext>\n      </OJPStopEventDelivery>\n    </siri:ServiceDelivery>\n  </OJPResponse>\n</OJP>"
  },
  {
    "path": "test/resources/ojp/trip_info_request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">\n<OJPRequest>\n  <siri:ServiceRequest>\n    <siri:ServiceRequestContext>\n      <siri:Language>de</siri:Language>\n    </siri:ServiceRequestContext>\n    <siri:RequestTimestamp>2026-01-28T07:42:51.310Z</siri:RequestTimestamp>\n    <siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>\n    <OJPTripInfoRequest>\n      <siri:RequestTimestamp>2026-01-28T07:42:51.310Z</siri:RequestTimestamp>\n      <JourneyRef>20190501_00:35_test_ICE</JourneyRef>\n      <OperatingDayRef>2019-05-01</OperatingDayRef>\n      <Params>\n        <IncludeCalls>true</IncludeCalls>\n        <IncludeService>true</IncludeService>\n        <IncludeTrackProjection>true</IncludeTrackProjection>\n        <IncludePlacesContext>true</IncludePlacesContext>\n        <IncludeSituationsContext>true</IncludeSituationsContext>\n      </Params>\n    </OJPTripInfoRequest>\n  </siri:ServiceRequest>\n</OJPRequest>\n\n</OJP>\n"
  },
  {
    "path": "test/resources/ojp/trip_info_response.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<OJP xmlns:siri=\"http://www.siri.org.uk/siri\" xmlns=\"http://www.vdv.de/ojp\" version=\"2.0\">\n  <OJPResponse>\n    <siri:ServiceDelivery>\n      <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n      <siri:ProducerRef>MOTIS</siri:ProducerRef>\n      <siri:ResponseMessageIdentifier>MSG</siri:ResponseMessageIdentifier>\n      <OJPTripInfoDelivery>\n        <siri:ResponseTimestamp>NOW</siri:ResponseTimestamp>\n        <siri:DefaultLanguage>de</siri:DefaultLanguage>\n        <TripInfoResponseContext>\n          <Places>\n            <Place>\n              <StopPlace>\n                <siri:StopPlaceRef>test_DA</siri:StopPlaceRef>\n                <StopPlaceName>\n                  <Text xml:lang=\"de\">DA Hbf</Text>\n                </StopPlaceName>\n              </StopPlace>\n              <Name>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.63085</siri:Longitude>\n                <siri:Latitude>49.8726</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">DA Hbf</Text>\n                </StopPointName>\n                <ParentRef>test_DA</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">DA Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.62926</siri:Longitude>\n                <siri:Latitude>49.87336</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPlace>\n                <siri:StopPlaceRef>test_FFM</siri:StopPlaceRef>\n                <StopPlaceName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPlaceName>\n              </StopPlace>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66341</siri:Longitude>\n                <siri:Latitude>50.10701</siri:Latitude>\n              </GeoPosition>\n            </Place>\n            <Place>\n              <StopPoint>\n                <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                <StopPointName>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </StopPointName>\n                <ParentRef>test_FFM</ParentRef>\n              </StopPoint>\n              <Name>\n                <Text xml:lang=\"de\">FFM Hbf</Text>\n              </Name>\n              <GeoPosition>\n                <siri:Longitude>8.66118</siri:Longitude>\n                <siri:Latitude>50.10593</siri:Latitude>\n              </GeoPosition>\n            </Place>\n          </Places>\n          <Situations />\n        </TripInfoResponseContext>\n        <TripInfoResult>\n          <PreviousCall>\n            <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n            <StopPointName>\n              <Text xml:lang=\"de\">DA Hbf</Text>\n            </StopPointName>\n            <PlannedQuay>\n              <Text xml:lang=\"de\">10</Text>\n            </PlannedQuay>\n            <NameSuffix>\n              <Text xml:lang=\"de\">PLATFORM_ACCESS_WITHOUT_ASSISTANCE</Text>\n            </NameSuffix>\n            <ServiceArrival />\n            <ServiceDeparture>\n              <TimetabledTime>2019-04-30T22:35:00Z</TimetabledTime>\n              <EstimatedTime>2019-04-30T22:35:00Z</EstimatedTime>\n            </ServiceDeparture>\n            <Order>1</Order>\n          </PreviousCall>\n          <PreviousCall>\n            <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n            <StopPointName>\n              <Text xml:lang=\"de\">FFM Hbf</Text>\n            </StopPointName>\n            <PlannedQuay>\n              <Text xml:lang=\"de\">10</Text>\n            </PlannedQuay>\n            <NameSuffix>\n              <Text xml:lang=\"de\">PLATFORM_ACCESS_WITHOUT_ASSISTANCE</Text>\n            </NameSuffix>\n            <ServiceArrival>\n              <TimetabledTime>2019-04-30T22:45:00Z</TimetabledTime>\n              <EstimatedTime>2019-04-30T22:45:00Z</EstimatedTime>\n            </ServiceArrival>\n            <ServiceDeparture />\n            <Order>2</Order>\n          </PreviousCall>\n          <Service>\n            <OperatingDayRef>2019-05-01</OperatingDayRef>\n            <JourneyRef>20190501_00:35_test_ICE</JourneyRef>\n            <PublicCode>ICE</PublicCode>\n            <siri:LineRef>test_ICE</siri:LineRef>\n            <siri:DirectionRef>0</siri:DirectionRef>\n            <Mode>\n              <PtMode>rail</PtMode>\n              <siri:RailSubmode>highSpeedRail</siri:RailSubmode>\n            </Mode>\n            <PublishedServiceName>\n              <Text xml:lang=\"de\">ICE</Text>\n            </PublishedServiceName>\n            <TrainNumber></TrainNumber>\n            <OriginText>\n              <Text xml:lang=\"de\">DA Hbf</Text>\n            </OriginText>\n            <siri:OperatorRef>DB</siri:OperatorRef>\n            <DestinationStopPointRef>test_FFM_10</DestinationStopPointRef>\n            <DestinationText>\n              <Text xml:lang=\"de\">FFM Hbf</Text>\n            </DestinationText>\n          </Service>\n          <JourneyTrack>\n            <TrackSection>\n              <TrackSectionStart>\n                <siri:StopPointRef>test_DA_10</siri:StopPointRef>\n                <Name>\n                  <Text xml:lang=\"de\">DA Hbf</Text>\n                </Name>\n              </TrackSectionStart>\n              <TrackSectionEnd>\n                <siri:StopPointRef>test_FFM_10</siri:StopPointRef>\n                <Name>\n                  <Text xml:lang=\"de\">FFM Hbf</Text>\n                </Name>\n              </TrackSectionEnd>\n              <LinkProjection></LinkProjection>\n              <Duration>PT10M</Duration>\n              <Length>0</Length>\n            </TrackSection>\n          </JourneyTrack>\n        </TripInfoResult>\n      </OJPTripInfoDelivery>\n    </siri:ServiceDelivery>\n  </OJPResponse>\n</OJP>"
  },
  {
    "path": "test/resources/test_case.geojson",
    "content": "{\n  \"type\": \"FeatureCollection\",\n  \"features\": [\n    {\n      \"type\": \"Feature\",\n      \"properties\": {},\n      \"geometry\": {\n        \"coordinates\": [\n          [\n            [\n              [\n                8.628286687808867,\n                49.87538775650398\n              ],\n              [\n                8.627729587531803,\n                49.875307377823475\n              ],\n              [\n                8.627688012884107,\n                49.87204389041574\n              ],\n              [\n                8.629583816813295,\n                49.8703022017942\n              ],\n              [\n                8.631820532852942,\n                49.87093993507034\n              ],\n              [\n                8.632028406090228,\n                49.87524843337286\n              ],\n              [\n                8.628286687808867,\n                49.87538775650398\n              ]\n            ]\n          ],\n          [\n            [\n              [\n                8.659725253456145,\n                50.10759331995783\n              ],\n              [\n                8.656418533851479,\n                50.10612497067021\n              ],\n              [\n                8.656811133317234,\n                50.10409793307585\n              ],\n              [\n                8.660305268566447,\n                50.103896483660066\n              ],\n              [\n                8.667076092035131,\n                50.105113219822016\n              ],\n              [\n                8.666971080055504,\n                50.10788252455177\n              ],\n              [\n                8.662900321548136,\n                50.10907497984371\n              ],\n              [\n                8.659725253456145,\n                50.10759331995783\n              ]\n            ]\n          ],\n          [\n          [\n            [\n              8.675250073298457,\n              50.11486984518109\n            ],\n            [\n              8.675183699890653,\n              50.11167759014242\n            ],\n            [\n              8.678274804325156,\n              50.111671509453686\n            ],\n            [\n              8.68149865557524,\n              50.11412196451832\n            ],\n            [\n              8.675250073298457,\n              50.11486984518109\n            ]\n          ]\n        ]\n        ],\n        \"type\": \"MultiPolygon\"\n      }\n    }\n  ]\n}\n\n"
  },
  {
    "path": "test/routing_shrink_results_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"motis/endpoints/routing.h\"\n\nusing namespace std::chrono_literals;\nusing namespace date;\nusing namespace motis::ep;\nnamespace n = nigiri;\nusing iv = n::interval<n::unixtime_t>;\n\nTEST(motis, shrink) {\n  auto const d = date::sys_days{2025_y / September / 29};\n  {\n    auto const j = [](std::uint8_t const transfers, n::unixtime_t const dep,\n                      n::unixtime_t const arr) {\n      auto x = n::routing::journey{};\n      x.start_time_ = dep;\n      x.dest_time_ = arr;\n      x.transfers_ = transfers;\n      return x;\n    };\n\n    auto journeys = std::vector<n::routing::journey>{\n        j(2, d + 10h, d + 11h),  //\n        j(1, d + 10h, d + 12h),  //\n        j(0, d + 10h, d + 13h),  //\n        j(2, d + 20h, d + 21h),  //\n        j(1, d + 20h, d + 22h),  //\n        j(0, d + 20h, d + 23h),  //\n    };\n\n    auto const i4 = shrink(false, 4, {d + 11h, d + 20h}, journeys);\n    EXPECT_EQ(6, journeys.size());\n    EXPECT_EQ((iv{d + 11h, d + 20h}), i4);\n\n    auto const i3 = shrink(false, 3, {d + 11h, d + 13h}, journeys);\n    EXPECT_EQ(3, journeys.size());\n    EXPECT_EQ((iv{d + 11h, d + 20h}), i3);\n\n    auto const i2 = shrink(false, 2, {d + 11h, d + 13h}, journeys);\n    EXPECT_EQ(3, journeys.size());\n    EXPECT_EQ((iv{d + 11h, d + 13h}), i2);\n\n    auto const i1 = shrink(false, 1, {d + 11h, d + 13h}, journeys);\n    EXPECT_EQ(3, journeys.size());\n    EXPECT_EQ((iv{d + 11h, d + 13h}), i1);\n  }\n\n  {\n    auto const j = [](std::uint8_t const transfers, n::unixtime_t const dep,\n                      n::unixtime_t const arr) {\n      auto x = n::routing::journey{};\n      x.start_time_ = arr;\n      x.dest_time_ = dep;\n      x.transfers_ = transfers;\n      return x;\n    };\n\n    auto journeys = std::vector<n::routing::journey>{\n        j(2, d + 10h, d + 11h),\n        j(1, d + 10h, d + 12h),\n        j(0, d + 10h, d + 13h),\n    };\n\n    auto const i3 = shrink(true, 3, {d + 11h, d + 13h}, journeys);\n    EXPECT_EQ(3, journeys.size());\n    EXPECT_EQ((iv{d + 11h, d + 13h}), i3);\n\n    auto const i2 = shrink(true, 2, {d + 11h, d + 13h}, journeys);\n    EXPECT_EQ((std::vector<n::routing::journey>{\n                  j(1, d + 10h, d + 12h),\n                  j(0, d + 10h, d + 13h),\n              }),\n              journeys);\n    EXPECT_EQ(2, journeys.size());\n    EXPECT_EQ((iv{d + 11h + 1min, d + 13h}), i2);\n\n    auto const i1 = shrink(true, 1, {d + 11h, d + 13h}, journeys);\n    EXPECT_EQ((std::vector<n::routing::journey>{\n                  j(0, d + 10h, d + 13h),\n              }),\n              journeys);\n    EXPECT_EQ(1, journeys.size());\n    EXPECT_EQ((iv{d + 12h + 1min, d + 13h}), i1);\n  }\n}\n"
  },
  {
    "path": "test/routing_slow_direct_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/config.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/import.h\"\n\n#include \"./util.h\"\n\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\nusing namespace std::chrono_literals;\n\nconstexpr auto const kSlowDirectGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nICE,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nICE,S1,ICE,,\nICE,S1,ICE2,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nICE,00:35:00,00:35:00,DA_10,0,0,1\nICE,00:45:00,00:45:00,FFM_10,1,1,0\nICE2,00:35:00,00:35:00,DA_10,0,0,1\nICE2,00:45:00,00:45:00,FFM_10,1,1,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n\n# frequencies.txt\ntrip_id,start_time,end_time,headway_secs\nICE,00:35:00,24:35:00,3600\nICE2,00:35:00,24:35:00,3600\n)\"sv;\n\nTEST(motis, routing_slow_direct) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = config{\n      .server_ = {{.web_folder_ = \"ui/build\", .n_threads_ = 1U}},\n      .osm_ = {\"test/resources/test_case.osm.pbf\"},\n      .tiles_ = {{.profile_ = \"deps/tiles/profile/full.lua\",\n                  .db_size_ = 1024U * 1024U * 25U}},\n      .timetable_ =\n          config::timetable{\n              .first_day_ = \"2019-05-01\",\n              .num_days_ = 2,\n              .use_osm_stop_coordinates_ = true,\n              .extend_missing_footpaths_ = false,\n              .datasets_ = {{\"test\", {.path_ = std::string{kSlowDirectGTFS}}}}},\n      .gbfs_ = {{.feeds_ = {{\"CAB\", {.url_ = \"./test/resources/gbfs\"}}}}},\n      .street_routing_ = true,\n      .osr_footpath_ = true,\n      .geocoding_ = true,\n      .reverse_geocoding_ = true};\n\n  import(c, \"test/data_osm_only\");\n  auto d = data{\"test/data_osm_only\", c};\n\n  auto const routing = utl::init_from<ep::routing>(d).value();\n\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87336,8.62926\"\n        \"&toPlace=test_FFM_10\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&slowDirect=false\");\n    ASSERT_TRUE(res.itineraries_.size() >= 2);\n    EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_,\n              \"20190501_03:35_test_ICE2\");\n    EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_,\n              \"20190501_04:35_test_ICE2\");\n  }\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87336,8.62926\"\n        \"&toPlace=test_FFM_10\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&slowDirect=true\");\n    ASSERT_TRUE(res.itineraries_.size() >= 2);\n    EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_,\n              \"20190501_03:35_test_ICE2\");\n    EXPECT_EQ(\n        res.itineraries_.at(1).legs_.at(1).tripId_,\n        \"20190501_03:35_test_ICE2\");  // this contains an addional 1min footpath\n    EXPECT_EQ(res.itineraries_.at(2).legs_.at(1).tripId_,\n              \"20190501_03:35_test_ICE\");\n  }\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87336,8.62926\"\n        \"&toPlace=test_FFM_10\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&slowDirect=true\"\n        \"&arriveBy=true\"\n        \"&numItineraries=2&maxItineraries=2\");\n    ASSERT_TRUE(res.itineraries_.size() >= 2);\n    EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_,\n              \"20190501_02:35_test_ICE2\");\n    EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_,\n              \"20190501_02:35_test_ICE\");\n  }\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87336,8.62926\"\n        \"&toPlace=test_FFM_10\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&slowDirect=true\"\n        \"&numItineraries=2&maxItineraries=2\"\n        \"&pageCursor=EARLIER%7C1556674200\");\n    ASSERT_TRUE(res.itineraries_.size() >= 2);\n    EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_,\n              \"20190501_02:35_test_ICE2\");\n    EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_,\n              \"20190501_02:35_test_ICE\");\n  }\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87336,8.62926\"\n        \"&toPlace=test_FFM_10\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&slowDirect=true\"\n        \"&arriveBy=true\"\n        \"&numItineraries=2&maxItineraries=2\"\n        \"&pageCursor=LATER%7C1556674200\");\n    ASSERT_TRUE(res.itineraries_.size() >= 2);\n    EXPECT_EQ(res.itineraries_.at(0).legs_.at(1).tripId_,\n              \"20190501_03:35_test_ICE2\");\n    EXPECT_EQ(res.itineraries_.at(1).legs_.at(1).tripId_,\n              \"20190501_03:35_test_ICE\");\n  }\n}\n"
  },
  {
    "path": "test/routing_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include <chrono>\n#include <sstream>\n\n#include \"boost/asio/co_spawn.hpp\"\n#include \"boost/asio/detached.hpp\"\n#include \"boost/json.hpp\"\n\n#ifdef NO_DATA\n#undef NO_DATA\n#endif\n#include \"gtfsrt/gtfs-realtime.pb.h\"\n\n#include \"utl/init_from.h\"\n\n#include \"nigiri/rt/gtfsrt_update.h\"\n\n#include \"motis-api/motis-api.h\"\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/elevators/elevators.h\"\n#include \"motis/elevators/parse_fasta.h\"\n#include \"motis/endpoints/routing.h\"\n#include \"motis/gbfs/update.h\"\n#include \"motis/import.h\"\n\n#include \"./util.h\"\n\nnamespace json = boost::json;\nusing namespace std::string_view_literals;\nusing namespace motis;\nusing namespace date;\nusing namespace std::chrono_literals;\nnamespace n = nigiri;\n\nconstexpr auto const kFastaJson = R\"__(\n[\n  {\n    \"description\": \"FFM HBF zu Gleis 101/102 (S-Bahn)\",\n    \"equipmentnumber\" : 10561326,\n    \"geocoordX\" : 8.6628995,\n    \"geocoordY\" : 50.1072933,\n    \"operatorname\" : \"DB InfraGO\",\n    \"state\" : \"ACTIVE\",\n    \"stateExplanation\" : \"available\",\n    \"stationnumber\" : 1866,\n    \"type\" : \"ELEVATOR\",\n    \"outOfService\": [\n      [\"2019-05-01T01:30:00Z\", \"2019-05-01T02:30:00Z\"]\n    ]\n  },\n  {\n    \"description\": \"FFM HBF zu Gleis 103/104 (S-Bahn)\",\n    \"equipmentnumber\": 10561327,\n    \"geocoordX\": 8.6627516,\n    \"geocoordY\": 50.1074549,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1866,\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"HAUPTWACHE zu Gleis 2/3 (S-Bahn)\",\n    \"equipmentnumber\": 10351032,\n    \"geocoordX\": 8.67818,\n    \"geocoordY\": 50.114046,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1864,\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"DA HBF zu Gleis 1\",\n    \"equipmentnumber\": 10543458,\n    \"geocoordX\": 8.6303864,\n    \"geocoordY\": 49.8725612,\n    \"state\": \"ACTIVE\",\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"DA HBF zu Gleis 3/4\",\n    \"equipmentnumber\": 10543453,\n    \"geocoordX\": 8.6300911,\n    \"geocoordY\": 49.8725678,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1126,\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"zu Gleis 5/6\",\n    \"equipmentnumber\": 10543454,\n    \"geocoordX\": 8.6298163,\n    \"geocoordY\": 49.8725555,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1126,\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"zu Gleis 7/8\",\n    \"equipmentnumber\": 10543455,\n    \"geocoordX\": 8.6295535,\n    \"geocoordY\": 49.87254,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1126,\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"zu Gleis 9/10\",\n    \"equipmentnumber\": 10543456,\n    \"geocoordX\": 8.6293117,\n    \"geocoordY\": 49.8725263,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1126,\n    \"type\": \"ELEVATOR\"\n  },\n  {\n    \"description\": \"zu Gleis 11/12\",\n    \"equipmentnumber\": 10543457,\n    \"geocoordX\": 8.6290451,\n    \"geocoordY\": 49.8725147,\n    \"operatorname\": \"DB InfraGO\",\n    \"state\": \"ACTIVE\",\n    \"stateExplanation\": \"available\",\n    \"stationnumber\": 1126,\n    \"type\": \"ELEVATOR\"\n  }\n]\n)__\"sv;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA_10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\nFFM_HAUPT_U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3,DB,S3,,,109\nU4,DB,U4,,,402\nICE,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3,S1,S3,,\nU4,S1,U4,,\nICE,S1,ICE,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nS3,01:15:00,01:15:00,FFM_101,1,0,0\nS3,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0\nU4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nU4,01:10:00,01:10:00,FFM_HAUPT_U,1,0,0\nICE,00:35:00,00:35:00,DA_10,0,0,0\nICE,00:45:00,00:45:00,FFM_10,1,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n\n# frequencies.txt\ntrip_id,start_time,end_time,headway_secs\nS3,01:15:00,25:15:00,3600\nICE,00:35:00,24:35:00,3600\nU4,01:05:00,25:01:00,3600\n)\"sv;\n\nvoid print_short(std::ostream& out, api::Itinerary const& j) {\n  auto const format_time = [&](auto&& t, char const* fmt = \"%F %H:%M\") {\n    out << date::format(fmt, *t);\n  };\n  auto const format_duration = [&](auto&& t, char const* fmt = \"%H:%M\") {\n    out << date::format(fmt, std::chrono::milliseconds{t});\n  };\n\n  out << \"date=\";\n  format_time(j.startTime_, \"%F\");\n  out << \", start=\";\n  format_time(j.startTime_, \"%H:%M\");\n  out << \", end=\";\n  format_time(j.endTime_, \"%H:%M\");\n\n  out << \", duration=\";\n  format_duration(j.duration_ * 1000U);\n  out << \", transfers=\" << j.transfers_;\n\n  out << \", legs=[\\n\";\n  auto first = true;\n  for (auto const& leg : j.legs_) {\n    if (!first) {\n      out << \",\\n    \";\n    } else {\n      out << \"    \";\n    }\n    first = false;\n\n    auto const print_alerts = [&](auto&& x) {\n      if (x.alerts_) {\n        auto first_alert = true;\n        out << \", alerts=[\";\n        for (auto const& a : *x.alerts_) {\n          if (!first_alert) {\n            out << \", \";\n          }\n          first_alert = false;\n          out << \"\\\"\" << a.headerText_ << \"\\\"\";\n        }\n        out << \"]\";\n      }\n    };\n\n    auto const print_stop = [&](api::Place const& p) {\n      out << p.stopId_.value_or(\"-\") << \" [track=\" << p.track_.value_or(\"-\")\n          << \", scheduled_track=\" << p.scheduledTrack_.value_or(\"-\")\n          << \", level=\" << p.level_;\n      print_alerts(p);\n      out << \"]\";\n    };\n\n    out << \"(\";\n    out << \"from=\";\n    print_stop(leg.from_);\n    out << \", to=\";\n    print_stop(leg.to_);\n    out << \", start=\";\n    format_time(leg.startTime_);\n    out << \", mode=\\\"\" << leg.mode_ << \"\\\", trip=\\\"\"\n        << leg.routeShortName_.value_or(\"-\") << \"\\\"\";\n    out << \", end=\";\n    format_time(leg.endTime_);\n    print_alerts(leg);\n\n    out << \")\";\n  }\n  out << \"\\n]\";\n}\n\nstd::string to_str(std::vector<api::Itinerary> const& x) {\n  auto ss = std::stringstream{};\n  for (auto const& j : x) {\n    print_short(ss, j);\n  }\n  return ss.str();\n}\n\nTEST(motis, routing_osm_only_direct_walk) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data_osm_only\", ec);\n\n  auto const c =\n      config{.server_ = {{.web_folder_ = \"ui/build\", .n_threads_ = 1U}},\n             .osm_ = {\"test/resources/test_case.osm.pbf\"},\n             .street_routing_ = true};\n  import(c, \"test/data_osm_only\");\n  auto d = data{\"test/data_osm_only\", c};\n\n  auto const routing = utl::init_from<ep::routing>(d).value();\n  auto const res = routing(\n      \"?fromPlace=49.87526849014631,8.62771903392948\"\n      \"&toPlace=49.87253873915287,8.629724234688751\"\n      \"&time=2019-05-01T01:25Z\"\n      \"&timetableView=false\"\n      \"&transitModes=\"\n      \"&directModes=WALK\");\n\n  ASSERT_TRUE(res.itineraries_.empty());\n  ASSERT_EQ(1U, res.direct_.size());\n  ASSERT_EQ(1U, res.direct_.front().legs_.size());\n  EXPECT_EQ(api::ModeEnum::WALK, res.direct_.front().legs_.front().mode_);\n}\n\nTEST(motis, routing) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = config{\n      .server_ = {{.web_folder_ = \"ui/build\", .n_threads_ = 1U}},\n      .osm_ = {\"test/resources/test_case.osm.pbf\"},\n      .tiles_ = {{.profile_ = \"deps/tiles/profile/full.lua\",\n                  .db_size_ = 1024U * 1024U * 25U}},\n      .timetable_ =\n          config::timetable{\n              .first_day_ = \"2019-05-01\",\n              .num_days_ = 2,\n              .use_osm_stop_coordinates_ = true,\n              .extend_missing_footpaths_ = false,\n              .datasets_ = {{\"test\", {.path_ = std::string{kGTFS}}}}},\n      .gbfs_ = {{.feeds_ = {{\"CAB\", {.url_ = \"./test/resources/gbfs\"}}}}},\n      .street_routing_ = true,\n      .osr_footpath_ = true,\n      .geocoding_ = true,\n      .reverse_geocoding_ = true};\n  import(c, \"test/data\");\n  auto d = data{\"test/data\", c};\n  d.rt_->e_ = std::make_unique<elevators>(*d.w_, nullptr, *d.elevator_nodes_,\n                                          parse_fasta(kFastaJson));\n  d.init_rtt(date::sys_days{2019_y / May / 1});\n\n  {\n    auto ioc = boost::asio::io_context{};\n    boost::asio::co_spawn(\n        ioc,\n        [&]() -> boost::asio::awaitable<void> {\n          co_await gbfs::update(c, *d.w_, *d.l_, d.gbfs_);\n        },\n        boost::asio::detached);\n    ioc.run();\n  }\n\n  auto const stats = n::rt::gtfsrt_update_msg(\n      *d.tt_, *d.rt_->rtt_, n::source_idx_t{0}, \"test\",\n      test::to_feed_msg(\n          {test::trip_update{.trip_ = {.trip_id_ = \"ICE\",\n                                       .start_time_ = {\"03:35:00\"},\n                                       .date_ = {\"20190501\"}},\n                             .stop_updates_ = {{.stop_id_ = \"FFM_12\",\n                                                .seq_ = std::optional{1U},\n                                                .ev_type_ = n::event_type::kArr,\n                                                .delay_minutes_ = 10,\n                                                .stop_assignment_ = \"FFM_12\"}}},\n           test::alert{.header_ = \"Yeah\",\n                       .description_ = \"Yeah!!\",\n                       .entities_ = {{.trip_ = {{.trip_id_ = \"ICE\",\n                                                 .start_time_ = {\"03:35:00\"},\n                                                 .date_ = {\"20190501\"}}},\n                                      .stop_id_ = \"DA\"}}},\n           test::alert{.header_ = \"Hello\",\n                       .description_ = \"World\",\n                       .entities_ = {{.trip_ = {{.trip_id_ = \"ICE\",\n                                                 .start_time_ = {\"03:35:00\"},\n                                                 .date_ = {\"20190501\"}}}}}}},\n          date::sys_days{2019_y / May / 1} + 9h));\n  EXPECT_EQ(1U, stats.total_entities_success_);\n  EXPECT_EQ(2U, stats.alert_total_resolve_success_);\n\n  auto const routing = utl::init_from<ep::routing>(d).value();\n  EXPECT_EQ(d.rt_->rtt_.get(), routing.rt_->rtt_.get());\n\n  // Route direct with GBFS.\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87526849014631,8.62771903392948\"\n        \"&toPlace=49.87253873915287,8.629724234688751\"\n        \"&time=2019-05-01T01:25Z\"\n        \"&timetableView=false\"\n        \"&directModes=WALK,RENTAL\");\n    ASSERT_FALSE(res.direct_.empty());\n    ASSERT_FALSE(res.direct_.front().legs_.empty());\n    EXPECT_GT(res.direct_.front().legs_.front().legGeometry_.length_, 0);\n    EXPECT_TRUE(res.direct_.front().legs_.front().steps_.has_value());\n\n    EXPECT_EQ(\n        R\"(date=2019-05-01, start=01:25, end=01:36, duration=00:11, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n]date=2019-05-01, start=01:25, end=01:36, duration=00:11, transfers=0, legs=[\n    (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)\n])\",\n        to_str(res.direct_));\n\n    auto const compact_res = routing(\n        \"?fromPlace=49.87526849014631,8.62771903392948\"\n        \"&toPlace=49.87253873915287,8.629724234688751\"\n        \"&time=2019-05-01T01:25Z\"\n        \"&timetableView=false\"\n        \"&directModes=WALK,RENTAL\"\n        \"&detailedLegs=false\");\n    ASSERT_FALSE(compact_res.direct_.empty());\n    for (auto const& itinerary : compact_res.direct_) {\n      for (auto const& leg : itinerary.legs_) {\n        EXPECT_EQ(\"\", leg.legGeometry_.points_);\n        EXPECT_EQ(0, leg.legGeometry_.length_);\n        EXPECT_FALSE(leg.steps_.has_value());\n      }\n    }\n  }\n\n  // Route with GBFS.\n  {\n    auto const res = routing(\n        \"?fromPlace=49.875308,8.6276673\"\n        \"&toPlace=50.11347,8.67664\"\n        \"&time=2019-05-01T01:21Z\"\n        \"&timetableView=false\"\n        \"&useRoutedTransfers=true\"\n        \"&preTransitModes=WALK,RENTAL\");\n\n    EXPECT_EQ(\n        R\"(date=2019-05-01, start=01:21, end=02:15, duration=00:54, transfers=1, legs=[\n    (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),\n    (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),\n    (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),\n    (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\"]),\n    (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),\n    (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),\n    (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)\n])\",\n        to_str(res.itineraries_));\n  }\n\n  // Routing with temporary blocked paths due to elevator being out of service\n  // Queries will use the wheelchair profile and have a long walking path before\n  // or after the elevator, to identify possible bugs\n  {\n    // Blocked near fromPlace, arriveBy=false, pass before blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=50.1040763,8.6586978\"\n          \"&toPlace=50.1132737,8.6767235\"\n          \"&time=2019-05-01T01:15Z\"\n          \"&arriveBy=false\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=01:16, end=02:29, duration=01:14, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near fromPlace, arriveBy=false, temporary blocked / must wait\n    {\n      auto const res = routing(\n          \"?fromPlace=50.1040763,8.6586978\"\n          \"&toPlace=50.1132737,8.6767235\"\n          \"&time=2019-05-01T01:20Z\"\n          \"&arriveBy=false\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=03:01, end=03:29, duration=02:09, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near fromPlace, arriveBy=true, must pass before blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=50.1040763,8.6586978\"\n          \"&toPlace=50.1132737,8.6767235\"\n          \"&time=2019-05-01T02:30Z\"\n          \"&arriveBy=true\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=01:16, end=02:29, duration=01:14, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near fromPlace, arriveBy=true, can pass after blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=50.1040763,8.6586978\"\n          \"&toPlace=50.1132737,8.6767235\"\n          \"&time=2019-05-01T03:30Z\"\n          \"&arriveBy=true\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=03:01, end=03:29, duration=00:29, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near toPlace, arriveBy=true, must pass before blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=49.87336,8.62926\"\n          \"&toPlace=50.106420,8.660708,-3\"\n          \"&time=2019-05-01T02:54Z\"\n          \"&arriveBy=true\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=01:34, end=02:40, duration=01:20, transfers=0, legs=[\n    (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),\n    (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\"]),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near toPlace, arriveBy=true, can pass after blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=49.87336,8.62926\"\n          \"&toPlace=50.106420,8.660708,-3\"\n          \"&time=2019-05-01T02:55Z\"\n          \"&arriveBy=true\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=02:34, end=02:55, duration=00:21, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near toPlace, arriveBy=false, temporary blocked / must wait\n    {\n      auto const res = routing(\n          \"?fromPlace=49.87336,8.62926\"\n          \"&toPlace=50.106420,8.660708,-3\"\n          \"&time=2019-05-01T01:30Z\"\n          \"&arriveBy=false\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match 'toPlace' closely\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=01:34, end=02:40, duration=01:10, transfers=0, legs=[\n    (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),\n    (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\"]),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Blocked near toPlace, arriveBy=false, can pass after blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=49.87336,8.62926\"\n          \"&toPlace=50.106420,8.660708,-3\"\n          \"&time=2019-05-01T01:40Z\"\n          \"&arriveBy=false\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match closely for wheelchair\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=02:34, end=02:55, duration=01:15, transfers=0, legs=[\n    (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),\n    (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),\n    (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)\n])\",\n          to_str(res.itineraries_));\n    }\n\n    // Direct routing, arriveBy=false, pass before blocked\n    {\n      auto const res = routing(\n          \"?fromPlace=50.10411515,8.658776549999999\"\n          \"&toPlace=50.106420,8.660708,-3\"\n          \"&time=2019-05-01T01:15Z\"\n          \"&arriveBy=false\"\n          \"&preTransitModes=WALK\"\n          \"&timetableView=false\"\n          \"&pedestrianProfile=WHEELCHAIR\"\n          \"&maxMatchingDistance=8\"  // Should match places closely\n          \"&useRoutedTransfers=true\");\n\n      EXPECT_EQ(\n          R\"(date=2019-05-01, start=01:15, end=01:32, duration=00:17, transfers=0, legs=[\n    (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)\n])\",\n          to_str(res.direct_));\n    }\n  }\n\n  // Route with wheelchair.\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87263,8.63127\"\n        \"&toPlace=50.11347,8.67664\"\n        \"&time=2019-05-01T01:25Z\"\n        \"&pedestrianProfile=WHEELCHAIR\"\n        \"&useRoutedTransfers=true\"\n        \"&timetableView=false\");\n\n    EXPECT_EQ(\n        R\"(date=2019-05-01, start=01:29, end=02:29, duration=01:04, transfers=1, legs=[\n    (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),\n    (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\"]),\n    (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),\n    (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),\n    (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)\n])\",\n        to_str(res.itineraries_));\n  }\n\n  // Route without wheelchair.\n  {\n    auto const res = routing(\n        \"?fromPlace=49.87263,8.63127\"\n        \"&toPlace=50.11347,8.67664\"\n        \"&time=2019-05-01T01:25Z\"\n        \"&useRoutedTransfers=true\"\n        \"&timetableView=false\");\n\n    EXPECT_EQ(\n        R\"(date=2019-05-01, start=01:25, end=02:15, duration=00:50, transfers=1, legs=[\n    (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),\n    (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\"]),\n    (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),\n    (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),\n    (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)\n])\",\n        to_str(res.itineraries_));\n  }\n\n  // Route using radius: finds stops within 5km radius from coordinates,\n  // no preTransitModes / OSM street routing needed.\n  {\n    // fromPlace is ~2km from DA_10, toPlace is ~2km from FFM_10.\n    auto const res = routing(\n        \"?fromPlace=49.89100,8.62900\"\n        \"&toPlace=50.08800,8.66100\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&radius=5000\"\n        \"&timetableView=false\");\n    ASSERT_FALSE(res.itineraries_.empty());\n    EXPECT_TRUE(utl::any_of(res.itineraries_.front().legs_, [](auto const& l) {\n      return l.routeShortName_.has_value() && *l.routeShortName_ == \"ICE\";\n    }));\n  }\n\n  // Route using radius on origin coordinate, station ID as destination.\n  {\n    // fromPlace is ~2km from DA_10, toPlace is the FFM_10 stop directly.\n    auto const res = routing(\n        \"?fromPlace=49.89100,8.62900\"\n        \"&toPlace=test_FFM_10\"\n        \"&time=2019-05-01T01:30Z\"\n        \"&radius=5000\"\n        \"&timetableView=false\");\n    ASSERT_FALSE(res.itineraries_.empty());\n    EXPECT_TRUE(utl::any_of(res.itineraries_.front().legs_, [](auto const& l) {\n      return l.routeShortName_.has_value() && *l.routeShortName_ == \"ICE\";\n    }));\n  }\n}\n"
  },
  {
    "path": "test/tag_lookup_test.cc",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"boost/url/url.hpp\"\n\n#include \"nigiri/types.h\"\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n#include \"motis/import.h\"\n#include \"motis/tag_lookup.h\"\n\nusing namespace std::string_view_literals;\nusing namespace osr;\n\nconstexpr auto const kGTFS = R\"(\n# agency.txt\nagency_id,agency_name,agency_url,agency_timezone\nDB,Deutsche Bahn,https://deutschebahn.com,Europe/Berlin\n\n# stops.txt\nstop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,platform_code\nDA,DA Hbf,49.87260,8.63085,1,,\nDA_3,DA Hbf,49.87355,8.63003,0,DA,3\nDA 10,DA Hbf,49.87336,8.62926,0,DA,10\nFFM,FFM Hbf,50.10701,8.66341,1,,\nFFM_101,FFM Hbf,50.10739,8.66333,0,FFM,101\nFFM_10,FFM Hbf,50.10593,8.66118,0,FFM,10\nFFM_12,FFM Hbf,50.10658,8.66178,0,FFM,12\nde:6412:10:6:1,FFM Hbf U-Bahn,50.107577,8.6638173,0,FFM,U4\nLANGEN,Langen,49.99359,8.65677,1,,1\nFFM_HAUPT,FFM Hauptwache,50.11403,8.67835,1,,\n+FFM_HÄUPT_&U,Hauptwache U1/U2/U3/U8,50.11385,8.67912,0,FFM_HAUPT,\nFFM_HAUPT_S,FFM Hauptwache S,50.11404,8.67824,0,FFM_HAUPT,\n\n# routes.txt\nroute_id,agency_id,route_short_name,route_long_name,route_desc,route_type\nS3 ,DB,S3,,,109\nÜ4,DB,U4,,,402\n+ICE_&A,DB,ICE,,,101\n\n# trips.txt\nroute_id,service_id,trip_id,trip_headsign,block_id\nS3 ,S1,S3 ,,\nÜ4,S1,Ü4,,\n+ICE_&A,S1,+ICE_&A,,\n\n# stop_times.txt\ntrip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\nS3 ,01:15:00,01:15:00,FFM_101,1,0,0\nS3 ,01:20:00,01:20:00,FFM_HAUPT_S,2,0,0\nÜ4,01:05:00,01:05:00,de:6412:10:6:1,0,0,0\nÜ4,01:10:00,01:10:00,+FFM_HÄUPT_&U,1,0,0\n+ICE_&A,00:35:00,00:35:00,DA 10,0,0,0\n+ICE_&A,00:45:00,00:45:00,FFM_10,1,0,0\n\n# calendar_dates.txt\nservice_id,date,exception_type\nS1,20190501,1\n)\"sv;\n\nTEST(motis, tag_lookup) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(\"test/data\", ec);\n\n  auto const c = motis::config{\n      .server_ = {{.web_folder_ = \"ui/build\", .n_threads_ = 1U}},\n      .osm_ = {\"test/resources/test_case.osm.pbf\"},\n      .timetable_ = motis::config::timetable{\n          .first_day_ = \"2019-05-01\",\n          .num_days_ = 2,\n          .datasets_ = {{\"test\", {.path_ = std::string{kGTFS}}}}}};\n  motis::import(c, \"test/data\");\n  auto d = motis::data{\"test/data\", c};\n  auto const rt = d.rt_;\n  auto const rtt = rt->rtt_.get();\n  EXPECT_TRUE(\n      d.tags_->get_trip(*d.tt_, rtt, \"20190501_01:15_test_S3 \").first.valid());\n  EXPECT_TRUE(\n      d.tags_->get_trip(*d.tt_, rtt, \"20190501_01:05_test_Ü4\").first.valid());\n  EXPECT_TRUE(d.tags_->get_trip(*d.tt_, rtt, \"20190501_00:35_test_+ICE_&A\")\n                  .first.valid());\n  EXPECT_NE(nigiri::location_idx_t::invalid(),\n            d.tags_->get_location(*d.tt_, \"test_DA 10\"));\n  EXPECT_NE(nigiri::location_idx_t::invalid(),\n            d.tags_->get_location(*d.tt_, \"test_+FFM_HÄUPT_&U\"));\n  EXPECT_NE(nigiri::location_idx_t::invalid(),\n            d.tags_->get_location(*d.tt_, \"test_DA 10\"));\n  auto u = boost::urls::url{\n      \"/api\",\n  };\n  u.params({true, false, false}).append({\"encoded\", \"a+& b\"});\n  u.params({false, false, false}).append({\"unencoded\", \"a+& b\"});\n  {\n    std::stringstream buffer;\n    buffer << u;\n    EXPECT_EQ(\"/api?encoded=a%2B%26+b&unencoded=a+%26%20b\", buffer.str());\n  }\n  {\n    std::stringstream buffer;\n    buffer << u.encoded_params();\n    EXPECT_EQ(\"encoded=a%2B%26+b&unencoded=a+%26%20b\", buffer.str());\n  }\n}\n"
  },
  {
    "path": "test/test_case/.gitignore",
    "content": "/*_data/\n"
  },
  {
    "path": "test/test_case.cc",
    "content": "#include \"./test_case.h\"\n\n#include \"motis/import.h\"\n\nusing motis::import;\n\ntest_case_params const import_test_case(config const&& c,\n                                        std::string_view path) {\n  auto ec = std::error_code{};\n  std::filesystem::remove_all(path, ec);\n\n  import(c, path);\n  return {path, std::move(c)};\n}\n"
  },
  {
    "path": "test/test_case.h",
    "content": "#pragma once\n\n/**\n * Framework for reusable tests\n *\n * Test cases need to be defined once and can then be used for multiple tests\n * See comments on how to add new test cases\n *\n * Reminder: Use with care! Try to reuse already existing tests first\n */\n\n#include <string_view>\n\n#include \"motis/config.h\"\n#include \"motis/data.h\"\n\nusing motis::config;\nusing motis::data;\n\n// Requires an element for each reusable test case\nenum class test_case {\n  FFM_one_to_many,\n};\n\nusing test_case_params = std::pair<std::string_view, config>;\n\n// Requires a specialisation for each test case\ntemplate <test_case TestCase>\ntest_case_params const import_test_case();\n\n// Most tests will only use 'data', but some might require access to 'config'\ntemplate <test_case TestCase>\nstd::pair<data, config const&> get_test_case() {\n  static auto const params{import_test_case<TestCase>()};\n  return {data{std::get<0>(params), std::get<1>(params)}, std::get<1>(params)};\n}\n\ntest_case_params const import_test_case(config const&&, std::string_view path);\n"
  },
  {
    "path": "test/test_dir.h.in",
    "content": "#pragma once\n\n#define OSR_TEST_EXECUTION_DIR \"@CMAKE_CURRENT_SOURCE_DIR@\""
  },
  {
    "path": "test/util.cc",
    "content": "#include \"./util.h\"\n\n#include \"fmt/format.h\"\n\nnamespace motis::test {\n\nusing namespace std::string_view_literals;\nusing namespace std::chrono_literals;\nusing namespace date;\n\nusing feed_entity = std::variant<trip_update, alert>;\n\ntransit_realtime::FeedMessage to_feed_msg(\n    std::vector<feed_entity> const& feed_entities,\n    date::sys_seconds const msg_time) {\n  transit_realtime::FeedMessage msg;\n\n  auto const hdr = msg.mutable_header();\n  hdr->set_gtfs_realtime_version(\"2.0\");\n  hdr->set_incrementality(\n      transit_realtime::FeedHeader_Incrementality_FULL_DATASET);\n  hdr->set_timestamp(to_unix(msg_time));\n\n  auto id = 0U;\n  for (auto const& x : feed_entities) {\n    auto const e = msg.add_entity();\n    e->set_id(fmt::format(\"{}\", ++id));\n\n    auto const set_trip = [](::transit_realtime::TripDescriptor* td,\n                             trip_descriptor const& trip,\n                             bool const canceled = false) {\n      td->set_trip_id(trip.trip_id_);\n      if (canceled) {\n        td->set_schedule_relationship(\n            transit_realtime::TripDescriptor_ScheduleRelationship_CANCELED);\n        return;\n      }\n      if (trip.date_) {\n        td->set_start_date(*trip.date_);\n      }\n      if (trip.start_time_) {\n        td->set_start_time(*trip.start_time_);\n      }\n    };\n\n    std::visit(\n        utl::overloaded{\n            [&](trip_update const& u) {\n              set_trip(e->mutable_trip_update()->mutable_trip(), u.trip_,\n                       u.cancelled_);\n\n              for (auto const& stop_upd : u.stop_updates_) {\n                auto* const upd =\n                    e->mutable_trip_update()->add_stop_time_update();\n                if (!stop_upd.stop_id_.empty()) {\n                  *upd->mutable_stop_id() = stop_upd.stop_id_;\n                }\n                if (stop_upd.seq_.has_value()) {\n                  upd->set_stop_sequence(*stop_upd.seq_);\n                }\n                if (stop_upd.stop_assignment_.has_value()) {\n                  upd->mutable_stop_time_properties()->set_assigned_stop_id(\n                      stop_upd.stop_assignment_.value());\n                }\n                if (stop_upd.skip_) {\n                  upd->set_schedule_relationship(\n                      transit_realtime::\n                          TripUpdate_StopTimeUpdate_ScheduleRelationship_SKIPPED);\n                  continue;\n                }\n                stop_upd.ev_type_ == ::nigiri::event_type::kDep\n                    ? upd->mutable_departure()->set_delay(\n                          stop_upd.delay_minutes_ * 60)\n                    : upd->mutable_arrival()->set_delay(\n                          stop_upd.delay_minutes_ * 60);\n              }\n            },\n\n            [&](alert const& a) {\n              auto const alert = e->mutable_alert();\n\n              auto const header =\n                  alert->mutable_header_text()->add_translation();\n              *header->mutable_text() = a.header_;\n              *header->mutable_language() = \"en\";\n\n              auto const description =\n                  alert->mutable_description_text()->add_translation();\n              *description->mutable_text() = a.description_;\n              *description->mutable_language() = \"en\";\n\n              for (auto const& entity : a.entities_) {\n                auto const ie = alert->add_informed_entity();\n                if (entity.agency_id_) {\n                  *ie->mutable_agency_id() = *entity.agency_id_;\n                }\n                if (entity.route_id_) {\n                  *ie->mutable_route_id() = *entity.route_id_;\n                }\n                if (entity.direction_id_) {\n                  ie->set_direction_id(*entity.direction_id_);\n                }\n                if (entity.route_type_) {\n                  ie->set_route_type(*entity.route_type_);\n                }\n                if (entity.stop_id_) {\n                  ie->set_stop_id(*entity.stop_id_);\n                }\n                if (entity.trip_) {\n                  set_trip(ie->mutable_trip(), *entity.trip_);\n                }\n              }\n            }},\n        x);\n  }\n\n  return msg;\n}\n\n}  // namespace motis::test"
  },
  {
    "path": "test/util.h",
    "content": "#include <chrono>\n\n#include \"date/date.h\"\n\n#include \"gtfsrt/gtfs-realtime.pb.h\"\n\n#include \"nigiri/types.h\"\n\nnamespace motis::test {\n\nusing namespace std::string_view_literals;\nusing namespace date;\nusing namespace std::chrono_literals;\n\nstruct trip_descriptor {\n  std::string trip_id_;\n  std::optional<std::string> start_time_;\n  std::optional<std::string> date_;\n};\n\nstruct trip_update {\n  struct stop_time_update {\n    std::string stop_id_;\n    std::optional<std::uint32_t> seq_{std::nullopt};\n    ::nigiri::event_type ev_type_{::nigiri::event_type::kDep};\n    std::int32_t delay_minutes_{0U};\n    bool skip_{false};\n    std::optional<std::string> stop_assignment_{std::nullopt};\n  };\n\n  trip_descriptor trip_;\n  std::vector<stop_time_update> stop_updates_{};\n  bool cancelled_{false};\n};\n\nstruct alert {\n  struct entity_selector {\n    std::optional<std::string> agency_id_{};\n    std::optional<std::string> route_id_{};\n    std::optional<std::int32_t> route_type_{};\n    std::optional<std::uint32_t> direction_id_{};\n    std::optional<trip_descriptor> trip_{};\n    std::optional<std::string> stop_id_{};\n  };\n  std::string header_;\n  std::string description_;\n  std::vector<entity_selector> entities_;\n};\n\nusing feed_entity = std::variant<trip_update, alert>;\n\ntemplate <typename T>\nstd::uint64_t to_unix(T&& x) {\n  return static_cast<std::uint64_t>(\n      std::chrono::time_point_cast<std::chrono::seconds>(x)\n          .time_since_epoch()\n          .count());\n};\n\ntransit_realtime::FeedMessage to_feed_msg(\n    std::vector<feed_entity> const& feed_entities,\n    date::sys_seconds const msg_time);\n\n}  // namespace motis::test"
  },
  {
    "path": "tools/buildcache-clang-tidy.lua",
    "content": "-- match(.*cmake.*)\n\n-------------------------------------------------------------------------------\n-- This is a re-implementation of the C++ class gcc_wrapper_t.\n-------------------------------------------------------------------------------\n\nrequire_std(\"io\")\nrequire_std(\"os\")\nrequire_std(\"string\")\nrequire_std(\"table\")\nrequire_std(\"bcache\")\n\n\n-------------------------------------------------------------------------------\n-- Internal helper functions.\n-------------------------------------------------------------------------------\n\nlocal function make_preprocessor_cmd (args, preprocessed_file)\n  local preprocess_args = {}\n\n  -- Drop arguments that we do not want/need.\n  local drop_next_arg = false\n  for i, arg in ipairs(args) do\n    local drop_this_arg = drop_next_arg\n    drop_next_arg = false\n    if arg == \"-c\" then\n      drop_this_arg = true\n    elseif arg == \"-o\" then\n      drop_this_arg = true\n      drop_next_arg = true\n    end\n    if not drop_this_arg and i > 6 then\n      table.insert(preprocess_args, arg)\n    end\n  end\n\n  -- Append the required arguments for producing preprocessed output.\n  table.insert(preprocess_args, \"-E\")\n  table.insert(preprocess_args, \"-P\")\n  table.insert(preprocess_args, \"-o\")\n  table.insert(preprocess_args, preprocessed_file)\n\n  return preprocess_args\nend\n\nlocal function is_source_file (path)\n  local ext = bcache.get_extension(path):lower()\n  return (ext == \".cpp\") or (ext == \".cc\") or (ext == \".cxx\") or (ext == \".c\")\nend\n\n\n-------------------------------------------------------------------------------\n-- Wrapper interface implementation.\n-------------------------------------------------------------------------------\n\nfunction get_capabilities ()\n  -- We can use hard links with GCC since it will never overwrite already\n  -- existing files.\n  return { \"hard_links\" }\nend\n\nfunction preprocess_source ()\n  local expected = {\"-E\", \"__run_co_compile\", \"--tidy\", \"--source\", \"--\"}\n  for i, arg in ipairs(expected) do\n    if string.sub(ARGS[i+1], 1, #expected[i]) ~= expected[i] then\n      error(\"Unexpected argument \" .. i .. \": \" .. ARGS[i+1] .. \" -> '\" .. string.sub(ARGS[i+1], 1, 1 + #expected[i]) .. \"' != '\" .. expected[i] .. \"'\")\n    end\n  end\n\n  -- Check if this is a compilation command that we support.\n  local is_object_compilation = false\n  local has_object_output = false\n  for i, arg in ipairs(ARGS) do\n    if arg == \"-c\" then\n      is_object_compilation = true\n    elseif arg == \"-o\" then\n      has_object_output = true\n    elseif arg:sub(1, 1) == \"@\" then\n      error(\"Response files are currently not supported.\")\n    end\n  end\n  if (not is_object_compilation) or (not has_object_output) then\n    error(\"Unsupported complation command.\")\n  end\n\n  -- Run the preprocessor step.\n  local preprocessed_file = os.tmpname()\n  local preprocessor_args = make_preprocessor_cmd(ARGS, preprocessed_file)\n\n  local result = bcache.run(preprocessor_args)\n  if result.return_code ~= 0 then\n    os.remove(preprocessed_file)\n    error(\"Preprocessing command was unsuccessful.\")\n  end\n\n  -- Read and return the preprocessed file.\n  local f = assert(io.open(preprocessed_file, \"rb\"))\n  local preprocessed_source = f:read(\"*all\")\n  f:close()\n  os.remove(preprocessed_file)\n\n  return preprocessed_source\nend\n\nfunction get_relevant_arguments ()\n  local filtered_args = {}\n\n  -- The first argument is the compiler binary without the path.\n  table.insert(filtered_args, bcache.get_file_part(ARGS[1]))\n\n  -- Note: We always skip the first arg since we have handled it already.\n  local skip_next_arg = true\n  for i, arg in ipairs(ARGS) do\n    if not skip_next_arg then\n      -- Does this argument specify a file (we don't want to hash those).\n      local is_arg_plus_file_name = (arg == \"-I\") or (arg == \"-MF\") or\n                                    (arg == \"-MT\") or (arg == \"-MQ\") or\n                                    (arg == \"-o\")\n\n      -- Generally unwanted argument (things that will not change how we go\n      -- from preprocessed code to binary object files)?\n      local first_two_chars = arg:sub(1, 2)\n      local is_unwanted_arg = (first_two_chars == \"-I\") or\n                              (first_two_chars == \"-D\") or\n                              (first_two_chars == \"-M\") or\n                              is_source_file(arg)\n\n      if is_arg_plus_file_name then\n        skip_next_arg = true\n      elseif not is_unwanted_arg then\n        table.insert(filtered_args, arg)\n      end\n    else\n      skip_next_arg = false\n    end\n  end\n\n  return filtered_args\nend\n\nfunction get_program_id ()\n  -- TODO(m): Add things like executable file size too.\n\n  -- Get the version string for the compiler.\n  local result = bcache.run({ARGS[1], \"--version\"})\n  if result.return_code ~= 0 then\n    error(\"Unable to get the compiler version information string.\")\n  end\n\n  return result.std_out\nend\n\nfunction get_build_files ()\n  local files = {}\n  local found_object_file = false\n  for i = 2, #ARGS do\n    local next_idx = i + 1\n    if (ARGS[i] == \"-o\") and (next_idx <= #ARGS) then\n      if found_object_file then\n        error(\"Only a single target object file can be specified.\")\n      end\n      files[\"object\"] = ARGS[next_idx]\n      found_object_file = true\n    elseif (ARGS[i] == \"-ftest-coverage\") then\n      error(\"Code coverage data is currently not supported.\")\n    end\n  end\n  if not found_object_file then\n    error(\"Unable to get the target object file.\")\n  end\n  return files\nend\n"
  },
  {
    "path": "tools/suppress.txt",
    "content": "{\n   uninitialized_values_written_to_memory_map\n   Memcheck:Param\n   msync(start)\n   fun:msync\n   fun:_ZN5cista4mmap4syncEv\n}\n{\n   lmdb_uninitialized_write_1\n   Memcheck:Param\n   pwrite64(buf)\n   fun:__libc_pwrite64\n   fun:pwrite\n   fun:mdb_page_flush\n   fun:mdb_txn_commit\n}\n{\n   lmdb_uninitialized_write_2\n   Memcheck:Param\n   writev(vector[0])\n   fun:__writev\n   fun:writev\n   fun:mdb_page_flush\n   fun:mdb_txn_commit\n}\n"
  },
  {
    "path": "tools/try-reproduce.py",
    "content": "#!/usr/bin/python3\n\nimport os\nimport sys\nimport subprocess\nimport shutil\nimport yaml\nfrom multiprocessing import Pool\nfrom pathlib import Path\n\nQUERIES = {\n    'raptor': {\n        'params': '?algorithm=RAPTOR&numItineraries=5&maxItineraries=5',\n        'exec': '/home/felix/code/motis/cmake-build-relwithdebinfo/motis'\n    },\n    'pong': {\n        'params': '?algorithm=PONG&numItineraries=5&maxItineraries=5',\n        'exec': '/home/felix/code/motis/cmake-build-relwithdebinfo/motis'\n    }\n}\n\n\ndef update_timetable_config(path):\n    with open(path, 'r') as file:\n        config = yaml.safe_load(file)\n\n    # config['timetable']['tb'] = True\n    config['timetable']['first_day'] = '2025-10-04'\n\n    with open(path, 'w') as file:\n        yaml.safe_dump(config, file)\n\n\ndef cmd(cmd, cwd=None, verbose=False):\n    try:\n        if verbose:\n            print(f\"Running: {' '.join(cmd)}\")\n            if cwd:\n                print(f\"Working directory: {cwd}\")\n\n        result = subprocess.run(cmd, cwd=cwd, check=True,\n                                capture_output=True,\n                                text=True)\n\n        if verbose:\n            if result.stdout:\n                print(\"STDOUT:\", result.stdout)\n            if result.stderr:\n                print(\"STDERR:\", result.stderr)\n\n        return result.returncode\n    except subprocess.CalledProcessError as e:\n        print(f\"Error running command: {' '.join(cmd)} [CWD={cwd}]\")\n        print(f\"Return code: {e.returncode}\")\n        if e.stdout:\n            print(f\"STDOUT: {e.stdout}\")\n        if e.stderr:\n            print(f\"STDERR: {e.stderr}\")\n\n\ndef get_directories(path):\n    \"\"\"Get all directories in the given path.\"\"\"\n    try:\n        return [d for d in os.listdir(path) if\n                d != 'data' and os.path.isdir(os.path.join(path, d))]\n    except FileNotFoundError:\n        print(f\"Directory {path} not found\")\n        return []\n\n\ndef try_reproduce(id_value, verbose=False):\n    motis = next(iter(QUERIES.items()))[1]['exec']\n    dir = f\"reproduce/{id_value}\"\n    os.makedirs(dir, exist_ok=True)\n\n    # Step 1: Execute motis extract\n    extract_cmd = [\n        motis,\n        \"extract\",\n        \"--filter_stops\", \"false\",\n        \"-i\",\n        f\"fail/{id_value}_0.json\"\n    ]\n\n    # Add second JSON file only if it exists\n    second_json = f\"fail/{id_value}_1.json\"\n    if os.path.exists(second_json):\n        extract_cmd.append(second_json)\n\n    extract_cmd.extend([\"-o\", dir, '--reduce', 'true'])\n\n    cmd(extract_cmd, verbose=verbose)\n\n    # Step 2: Get timetable directories and run motis import\n    timetable_dirs = get_directories(dir)\n    if not timetable_dirs:\n        print(f\"Warning: No timetable directories found in {id_value}\")\n        sys.exit(1)\n\n    cmd([motis, 'config'] + timetable_dirs, cwd=dir, verbose=verbose)\n    update_timetable_config(f\"{dir}/config.yml\")\n    cmd([motis, 'import'], cwd=dir, verbose=verbose)\n\n    # Step 3: Copy query file\n    for name, run in QUERIES.items():\n        cmd([\n            motis,\n            'params',\n            '-i', f\"../../fail/{id_value}_q.txt\",\n            '-o', f\"queries-{name}.txt\",\n            '-p', run['params']\n        ], cwd=dir, verbose=verbose)\n\n    # Step 4: Run Queries\n    for name, params in QUERIES.items():\n        cmd([\n            params['exec'],\n            'batch',\n            '-q', f\"queries-{name}.txt\",\n            '-r', f\"responses-{name}.txt\"\n        ], cwd=dir, verbose=verbose)\n\n    # Step 5: Compare\n    compare_cmd = [\n        motis,\n        'compare',\n        '-q', f\"queries-{next(iter(QUERIES))}.txt\",\n        '-r'\n    ]\n    compare_cmd.extend([\n        f\"responses-{name}.txt\" for name, params in QUERIES.items()\n    ])\n\n    if cmd(compare_cmd, cwd=dir, verbose=verbose) == 0:\n        if verbose:\n            print(\"NO DIFF\")\n        return False\n    else:\n        if verbose:\n            print(\"REPRODUCED\")\n        return True\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) == 2:\n        id_value = sys.argv[1]\n        try_reproduce(id_value, True)\n    else:\n        with Pool(processes=6) as pool:\n            query_ids = [d.removesuffix('_q.txt') for d in os.listdir('fail') if\n                         d.endswith('_q.txt')]\n            pool.map(try_reproduce, query_ids)\n"
  },
  {
    "path": "tools/ubsan-suppress.txt",
    "content": "src:*/LuaJIT/*\n"
  },
  {
    "path": "ui/.gitignore",
    "content": ".DS_Store\nnode_modules\n/build\n.vscode\n/.svelte-kit\n/package\n.env\n.env.*\n!.env.example\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n/api/dist\n"
  },
  {
    "path": "ui/.npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": "ui/.prettierignore",
    "content": "# Ignore files for PNPM, NPM and YARN\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\napi/**\nsrc/lib/components/**\nstatic/sprite*.json\nstatic/icons/**.svg\n.pnpm-store"
  },
  {
    "path": "ui/.prettierrc",
    "content": "{\n\t\"useTabs\": true,\n\t\"singleQuote\": true,\n\t\"trailingComma\": \"none\",\n\t\"printWidth\": 100,\n\t\"plugins\": [\"prettier-plugin-svelte\"],\n\t\"overrides\": [{ \"files\": \"*.svelte\", \"options\": { \"parser\": \"svelte\" } }]\n}\n"
  },
  {
    "path": "ui/README.md",
    "content": "Build UI (the `-r` is important to also update the OpenAPI client):\n\n```bash\npnpm -r build\n```\n\nGenerate OpenAPI client (when openapi.yaml has been changed, included in `pnpm -r build`):\n\n```bash\npnpm update-api\n```\n\nTo publish a new version to npmjs:\n\n```bash\ncd src/lib/api\npnpm build\npnpm version patch --no-git-tag-version\npnpm publish --access public\n```\n"
  },
  {
    "path": "ui/api/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 MOTIS Project\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "ui/api/README.md",
    "content": "# motis-client\n\nPre-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).\n\nFor example:\n\n```js\nconst response = await stoptimes({\n\tthrowOnError: true,\n\tbaseUrl: 'https://api.transitous.org',\n\theaders: {\n\t\t'User-Agent': 'my-user-agent'\n\t},\n\tquery: {\n\t\tstopId: 'de-DELFI_de:06412:7010:1:3',\n\t\tn: 10,\n\t\tradius: 500\n\t}\n});\nconsole.log(response);\n```\n"
  },
  {
    "path": "ui/api/openapi/index.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\nexport * from './schemas.gen';\nexport * from './services.gen';\nexport * from './types.gen';"
  },
  {
    "path": "ui/api/openapi/schemas.gen.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\n\nexport const AlertCauseSchema = {\n    description: 'Cause of this alert.',\n    type: 'string',\n    enum: ['UNKNOWN_CAUSE', 'OTHER_CAUSE', 'TECHNICAL_PROBLEM', 'STRIKE', 'DEMONSTRATION', 'ACCIDENT', 'HOLIDAY', 'WEATHER', 'MAINTENANCE', 'CONSTRUCTION', 'POLICE_ACTIVITY', 'MEDICAL_EMERGENCY']\n} as const;\n\nexport const AlertEffectSchema = {\n    description: 'The effect of this problem on the affected entity.',\n    type: 'string',\n    enum: ['NO_SERVICE', 'REDUCED_SERVICE', 'SIGNIFICANT_DELAYS', 'DETOUR', 'ADDITIONAL_SERVICE', 'MODIFIED_SERVICE', 'OTHER_EFFECT', 'UNKNOWN_EFFECT', 'STOP_MOVED', 'NO_EFFECT', 'ACCESSIBILITY_ISSUE']\n} as const;\n\nexport const AlertSeverityLevelSchema = {\n    description: 'The severity of the alert.',\n    type: 'string',\n    enum: ['UNKNOWN_SEVERITY', 'INFO', 'WARNING', 'SEVERE']\n} as const;\n\nexport const TimeRangeSchema = {\n    description: `A time interval.\nThe interval is considered active at time t if t is greater than or equal to the start time and less than the end time.\n`,\n    type: 'object',\n    required: ['start', 'end'],\n    properties: {\n        start: {\n            description: `If missing, the interval starts at minus infinity.\nIf a TimeRange is provided, either start or end must be provided - both fields cannot be empty.\n`,\n            type: 'string',\n            format: 'date-time'\n        },\n        end: {\n            description: `If missing, the interval ends at plus infinity.\nIf a TimeRange is provided, either start or end must be provided - both fields cannot be empty.\n`,\n            type: 'string',\n            format: 'date-time'\n        }\n    }\n} as const;\n\nexport const AlertSchema = {\n    description: 'An alert, indicating some sort of incident in the public transit network.',\n    type: 'object',\n    required: ['headerText', 'descriptionText'],\n    properties: {\n        code: {\n            type: 'string',\n            description: 'Attribute or notice code (e.g. for HRDF or NeTEx)'\n        },\n        communicationPeriod: {\n            description: `Time when the alert should be shown to the user.\nIf missing, the alert will be shown as long as it appears in the feed.\nIf multiple ranges are given, the alert will be shown during all of them.\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/TimeRange'\n            }\n        },\n        impactPeriod: {\n            description: 'Time when the services are affected by the disruption mentioned in the alert.',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/TimeRange'\n            }\n        },\n        cause: {\n            '$ref': '#/components/schemas/AlertCause'\n        },\n        causeDetail: {\n            type: 'string',\n            description: `Description of the cause of the alert that allows for agency-specific language;\nmore specific than the Cause.\n`\n        },\n        effect: {\n            '$ref': '#/components/schemas/AlertEffect'\n        },\n        effectDetail: {\n            type: 'string',\n            description: `Description of the effect of the alert that allows for agency-specific language;\nmore specific than the Effect.\n`\n        },\n        url: {\n            type: 'string',\n            description: 'The URL which provides additional information about the alert.'\n        },\n        headerText: {\n            type: 'string',\n            description: `Header for the alert. This plain-text string will be highlighted, for example in boldface.\n`\n        },\n        descriptionText: {\n            type: 'string',\n            description: `Description for the alert.\nThis plain-text string will be formatted as the body of the alert (or shown on an explicit \"expand\" request by the user).\nThe information in the description should add to the information of the header.\n`\n        },\n        ttsHeaderText: {\n            type: 'string',\n            description: `Text containing the alert's header to be used for text-to-speech implementations.\nThis field is the text-to-speech version of header_text.\nIt should contain the same information as headerText but formatted such that it can read as text-to-speech\n(for example, abbreviations removed, numbers spelled out, etc.)\n`\n        },\n        ttsDescriptionText: {\n            type: 'string',\n            description: `Text containing a description for the alert to be used for text-to-speech implementations.\nThis field is the text-to-speech version of description_text.\nIt should contain the same information as description_text but formatted such that it can be read as text-to-speech\n(for example, abbreviations removed, numbers spelled out, etc.)\n`\n        },\n        severityLevel: {\n            description: 'Severity of the alert.',\n            '$ref': '#/components/schemas/AlertSeverityLevel'\n        },\n        imageUrl: {\n            description: 'String containing an URL linking to an image.',\n            type: 'string'\n        },\n        imageMediaType: {\n            description: `IANA media type as to specify the type of image to be displayed. The type must start with \"image/\"\n`,\n            type: 'string'\n        },\n        imageAlternativeText: {\n            description: `Text describing the appearance of the linked image in the image field\n(e.g., in case the image can't be displayed or the user can't see the image for accessibility reasons).\nSee the HTML spec for alt image text.\n`,\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const DurationSchema = {\n    description: 'Object containing duration if a path was found or none if no path was found',\n    type: 'object',\n    properties: {\n        duration: {\n            type: 'number',\n            description: 'duration in seconds if a path was found, otherwise missing',\n            minimum: 0\n        },\n        distance: {\n            type: 'number',\n            description: 'distance in meters if a path was found and distance computation was requested, otherwise missing',\n            minimum: 0\n        }\n    }\n} as const;\n\nexport const ParetoSetEntrySchema = {\n    description: 'Object containing a single element of a ParetoSet',\n    type: 'object',\n    required: ['duration', 'transfers'],\n    properties: {\n        duration: {\n            type: 'number',\n            description: `duration in seconds for the the best solution using \\`transfer\\` transfers\n\nNotice that the resolution is currently in minutes, because of implementation details\n`,\n            minimum: 0\n        },\n        transfers: {\n            description: `The minimal number of transfers required to arrive within \\`duration\\` seconds\n\ntransfers=0: Direct transit connecion without any transfers\ntransfers=1: Transit connection with 1 transfer\n`,\n            type: 'integer',\n            minimum: 0\n        }\n    }\n} as const;\n\nexport const ParetoSetSchema = {\n    description: 'Pareto set of optimal transit solutions',\n    type: 'array',\n    items: {\n        '$ref': '#/components/schemas/ParetoSetEntry'\n    }\n} as const;\n\nexport const OneToManyIntermodalResponseSchema = {\n    description: 'Object containing the optimal street and transit durations for One-to-Many routing',\n    type: 'object',\n    properties: {\n        street_durations: {\n            description: `Fastest durations for street routing\nThe order of the items corresponds to the order of the \\`many\\` locations\nIf no street routed connection is found, the corresponding \\`Duration\\` will be empty\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Duration'\n            }\n        },\n        transit_durations: {\n            description: `Pareto optimal solutions\nThe order of the items corresponds to the order of the \\`many\\` locations\nIf no connection using transits is found, the corresponding \\`ParetoSet\\` will be empty\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/ParetoSet'\n            }\n        }\n    }\n} as const;\n\nexport const AreaSchema = {\n    description: 'Administrative area',\n    type: 'object',\n    required: ['name', 'adminLevel', 'matched'],\n    properties: {\n        name: {\n            type: 'string',\n            description: 'Name of the area'\n        },\n        adminLevel: {\n            type: 'number',\n            description: `[OpenStreetMap \\`admin_level\\`](https://wiki.openstreetmap.org/wiki/Key:admin_level)\nof the area\n`\n        },\n        matched: {\n            type: 'boolean',\n            description: 'Whether this area was matched by the input text'\n        },\n        unique: {\n            type: 'boolean',\n            description: `Set for the first area after the \\`default\\` area that distinguishes areas\nif the match is ambiguous regarding (\\`default\\` area + place name / street [+ house number]).\n`\n        },\n        default: {\n            type: 'boolean',\n            description: 'Whether this area should be displayed as default area (area with admin level closest 7)'\n        }\n    }\n} as const;\n\nexport const TokenSchema = {\n    description: 'Matched token range (from index, length)',\n    type: 'array',\n    minItems: 2,\n    maxItems: 2,\n    items: {\n        type: 'number'\n    }\n} as const;\n\nexport const LocationTypeSchema = {\n    description: 'location type',\n    type: 'string',\n    enum: ['ADDRESS', 'PLACE', 'STOP']\n} as const;\n\nexport const ModeSchema = {\n    description: `# Street modes\n\n  - \\`WALK\\`\n  - \\`BIKE\\`\n  - \\`RENTAL\\` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs.\n  - \\`CAR\\`\n  - \\`CAR_PARKING\\` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs.\n  - \\`CAR_DROPOFF\\` Experimental. Expect unannounced breaking changes (without version bumps) for all perameters and returned structs.\n  - \\`ODM\\` on-demand taxis from the Prima+ÖV Project\n  - \\`RIDE_SHARING\\` ride sharing from the Prima+ÖV Project\n  - \\`FLEX\\` flexible transports\n\n# Transit modes\n\n  - \\`TRANSIT\\`: translates to \\`TRAM,FERRY,AIRPLANE,BUS,COACH,RAIL,ODM,FUNICULAR,AERIAL_LIFT,OTHER\\`\n  - \\`TRAM\\`: trams\n  - \\`SUBWAY\\`: subway trains (Paris Metro, London Underground, but also NYC Subway, Hamburger Hochbahn, and other non-underground services)\n  - \\`FERRY\\`: ferries\n  - \\`AIRPLANE\\`: airline flights\n  - \\`BUS\\`: short distance buses (does not include \\`COACH\\`)\n  - \\`COACH\\`: long distance buses (does not include \\`BUS\\`)\n  - \\`RAIL\\`: translates to \\`HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN,SUBWAY\\`\n  - \\`HIGHSPEED_RAIL\\`: long distance high speed trains (e.g. TGV)\n  - \\`LONG_DISTANCE\\`: long distance inter city trains\n  - \\`NIGHT_RAIL\\`: long distance night trains\n  - \\`REGIONAL_FAST_RAIL\\`: deprecated, \\`REGIONAL_RAIL\\` will be used\n  - \\`REGIONAL_RAIL\\`: regional train\n  - \\`SUBURBAN\\`: suburban trains (e.g. S-Bahn, RER, Elizabeth Line, ...)\n  - \\`ODM\\`: demand responsive transport\n  - \\`FUNICULAR\\`: Funicular. Any rail system designed for steep inclines.\n  - \\`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.\n  - \\`AREAL_LIFT\\`: deprecated\n  - \\`METRO\\`: deprecated\n  - \\`CABLE_CAR\\`: deprecated\n`,\n    type: 'string',\n    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']\n} as const;\n\nexport const MatchSchema = {\n    description: 'GeoCoding match',\n    type: 'object',\n    required: ['type', 'name', 'id', 'lat', 'lon', 'tokens', 'areas', 'score'],\n    properties: {\n        type: {\n            '$ref': '#/components/schemas/LocationType'\n        },\n        category: {\n            description: `Experimental. Type categories might be adjusted.\n\nFor OSM stop locations: the amenity type based on\nhttps://wiki.openstreetmap.org/wiki/OpenStreetMap_Carto/Symbols\n`,\n            type: 'string'\n        },\n        tokens: {\n            description: 'list of non-overlapping tokens that were matched',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Token'\n            }\n        },\n        name: {\n            description: 'name of the location (transit stop / PoI / address)',\n            type: 'string'\n        },\n        id: {\n            description: 'unique ID of the location',\n            type: 'string'\n        },\n        lat: {\n            description: 'latitude',\n            type: 'number'\n        },\n        lon: {\n            description: 'longitude',\n            type: 'number'\n        },\n        level: {\n            description: `level according to OpenStreetMap\n(at the moment only for public transport)\n`,\n            type: 'number'\n        },\n        street: {\n            description: 'street name',\n            type: 'string'\n        },\n        houseNumber: {\n            description: 'house number',\n            type: 'string'\n        },\n        country: {\n            description: 'ISO3166-1 country code from OpenStreetMap',\n            type: 'string'\n        },\n        zip: {\n            description: 'zip code',\n            type: 'string'\n        },\n        tz: {\n            description: 'timezone name (e.g. \"Europe/Berlin\")',\n            type: 'string'\n        },\n        areas: {\n            description: 'list of areas',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Area'\n            }\n        },\n        score: {\n            description: 'score according to the internal scoring system (the scoring algorithm might change in the future)',\n            type: 'number'\n        },\n        modes: {\n            description: 'available transport modes for stops',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Mode'\n            }\n        },\n        importance: {\n            description: 'importance of a stop, normalized from [0, 1]',\n            type: 'number'\n        }\n    }\n} as const;\n\nexport const ElevationCostsSchema = {\n    description: `Different elevation cost profiles for street routing.\nUsing a elevation cost profile will prefer routes with a smaller incline and smaller difference in elevation, even if the routed way is longer.\n\n- \\`NONE\\`: Ignore elevation data for routing. This is the default behavior\n- \\`LOW\\`: Add a low penalty for inclines. This will favor longer paths, if the elevation increase and incline are smaller.\n- \\`HIGH\\`: Add a high penalty for inclines. This will favor even longer paths, if the elevation increase and incline are smaller.\n`,\n    type: 'string',\n    enum: ['NONE', 'LOW', 'HIGH']\n} as const;\n\nexport const PedestrianProfileSchema = {\n    description: 'Different accessibility profiles for pedestrians.',\n    type: 'string',\n    enum: ['FOOT', 'WHEELCHAIR']\n} as const;\n\nexport const PedestrianSpeedSchema = {\n    description: 'Average speed for pedestrian routing in meters per second',\n    type: 'number'\n} as const;\n\nexport const CyclingSpeedSchema = {\n    description: 'Average speed for bike routing in meters per second',\n    type: 'number'\n} as const;\n\nexport const VertexTypeSchema = {\n    type: 'string',\n    description: `- \\`NORMAL\\` - latitude / longitude coordinate or address\n- \\`BIKESHARE\\` - bike sharing station\n- \\`TRANSIT\\` - transit stop\n`,\n    enum: ['NORMAL', 'BIKESHARE', 'TRANSIT']\n} as const;\n\nexport const PickupDropoffTypeSchema = {\n    type: 'string',\n    description: `- \\`NORMAL\\` - entry/exit is possible normally\n- \\`NOT_ALLOWED\\` - entry/exit is not allowed\n`,\n    enum: ['NORMAL', 'NOT_ALLOWED']\n} as const;\n\nexport const PlaceSchema = {\n    type: 'object',\n    required: ['name', 'lat', 'lon', 'level'],\n    properties: {\n        name: {\n            description: 'name of the transit stop / PoI / address',\n            type: 'string'\n        },\n        stopId: {\n            description: \"The ID of the stop. This is often something that users don't care about.\",\n            type: 'string'\n        },\n        parentId: {\n            description: \"If it's not a root stop, this field contains the `stopId` of the parent stop.\",\n            type: 'string'\n        },\n        importance: {\n            description: 'The importance of the stop between 0-1.',\n            type: 'number'\n        },\n        lat: {\n            description: 'latitude',\n            type: 'number'\n        },\n        lon: {\n            description: 'longitude',\n            type: 'number'\n        },\n        level: {\n            description: 'level according to OpenStreetMap',\n            type: 'number'\n        },\n        tz: {\n            description: 'timezone name (e.g. \"Europe/Berlin\")',\n            type: 'string'\n        },\n        arrival: {\n            description: 'arrival time',\n            type: 'string',\n            format: 'date-time'\n        },\n        departure: {\n            description: 'departure time',\n            type: 'string',\n            format: 'date-time'\n        },\n        scheduledArrival: {\n            description: 'scheduled arrival time',\n            type: 'string',\n            format: 'date-time'\n        },\n        scheduledDeparture: {\n            description: 'scheduled departure time',\n            type: 'string',\n            format: 'date-time'\n        },\n        scheduledTrack: {\n            description: 'scheduled track from the static schedule timetable dataset',\n            type: 'string'\n        },\n        track: {\n            description: `The current track/platform information, updated with real-time updates if available. \nCan be missing if neither real-time updates nor the schedule timetable contains track information.\n`,\n            type: 'string'\n        },\n        description: {\n            description: 'description of the location that provides more detailed information',\n            type: 'string'\n        },\n        vertexType: {\n            '$ref': '#/components/schemas/VertexType'\n        },\n        pickupType: {\n            description: 'Type of pickup. It could be disallowed due to schedule, skipped stops or cancellations.',\n            '$ref': '#/components/schemas/PickupDropoffType'\n        },\n        dropoffType: {\n            description: 'Type of dropoff. It could be disallowed due to schedule, skipped stops or cancellations.',\n            '$ref': '#/components/schemas/PickupDropoffType'\n        },\n        cancelled: {\n            description: 'Whether this stop is cancelled due to the realtime situation.',\n            type: 'boolean'\n        },\n        alerts: {\n            description: 'Alerts for this stop.',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Alert'\n            }\n        },\n        flex: {\n            description: 'for `FLEX` transports, the flex location area or location group name',\n            type: 'string'\n        },\n        flexId: {\n            description: 'for `FLEX` transports, the flex location area ID or location group ID',\n            type: 'string'\n        },\n        flexStartPickupDropOffWindow: {\n            description: 'Time that on-demand service becomes available',\n            type: 'string',\n            format: 'date-time'\n        },\n        flexEndPickupDropOffWindow: {\n            description: 'Time that on-demand service ends',\n            type: 'string',\n            format: 'date-time'\n        },\n        modes: {\n            description: 'available transport modes for stops',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Mode'\n            }\n        }\n    }\n} as const;\n\nexport const ReachablePlaceSchema = {\n    description: 'Place reachable by One-to-All',\n    type: 'object',\n    properties: {\n        place: {\n            '$ref': '#/components/schemas/Place',\n            description: 'Place reached by One-to-All'\n        },\n        duration: {\n            type: 'integer',\n            description: 'Total travel duration'\n        },\n        k: {\n            type: 'integer',\n            description: `k is the smallest number, for which a journey with the shortest duration and at most k-1 transfers exist.\nYou can think of k as the number of connections used.\n\nIn more detail:\n\nk=0: No connection, e.g. for the one location\nk=1: Direct connection\nk=2: Connection with 1 transfer\n`\n        }\n    }\n} as const;\n\nexport const ReachableSchema = {\n    description: 'Object containing all reachable places by One-to-All search',\n    type: 'object',\n    properties: {\n        one: {\n            '$ref': '#/components/schemas/Place',\n            description: 'One location used in One-to-All search'\n        },\n        all: {\n            description: 'List of locations reachable by One-to-All',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/ReachablePlace'\n            }\n        }\n    }\n} as const;\n\nexport const StopTimeSchema = {\n    description: 'departure or arrival event at a stop',\n    type: 'object',\n    required: ['place', 'mode', 'realTime', 'headsign', 'tripFrom', 'tripTo', 'agencyId', 'agencyName', 'agencyUrl', 'tripId', 'routeId', 'directionId', 'routeShortName', 'routeLongName', 'tripShortName', 'displayName', 'pickupDropoffType', 'cancelled', 'tripCancelled', 'source'],\n    properties: {\n        place: {\n            '$ref': '#/components/schemas/Place',\n            description: 'information about the stop place and time'\n        },\n        mode: {\n            '$ref': '#/components/schemas/Mode',\n            description: 'Transport mode for this leg'\n        },\n        realTime: {\n            description: 'Whether there is real-time data about this leg',\n            type: 'boolean'\n        },\n        headsign: {\n            description: `The headsign of the bus or train being used.\nFor non-transit legs, null\n`,\n            type: 'string'\n        },\n        tripFrom: {\n            description: 'first stop of this trip',\n            '$ref': '#/components/schemas/Place'\n        },\n        tripTo: {\n            description: 'final stop of this trip',\n            '$ref': '#/components/schemas/Place'\n        },\n        agencyId: {\n            type: 'string'\n        },\n        agencyName: {\n            type: 'string'\n        },\n        agencyUrl: {\n            type: 'string'\n        },\n        routeId: {\n            type: 'string'\n        },\n        routeUrl: {\n            type: 'string'\n        },\n        directionId: {\n            type: 'string'\n        },\n        routeColor: {\n            type: 'string'\n        },\n        routeTextColor: {\n            type: 'string'\n        },\n        tripId: {\n            type: 'string'\n        },\n        routeType: {\n            type: 'integer'\n        },\n        routeShortName: {\n            type: 'string'\n        },\n        routeLongName: {\n            type: 'string'\n        },\n        tripShortName: {\n            type: 'string'\n        },\n        displayName: {\n            type: 'string'\n        },\n        previousStops: {\n            type: 'array',\n            description: `Experimental. Expect unannounced breaking changes (without version bumps).\n\nStops on the trips before this stop. Returned only if \\`fetchStop\\` and \\`arriveBy\\` are \\`true\\`.\n`,\n            items: {\n                '$ref': '#/components/schemas/Place'\n            }\n        },\n        nextStops: {\n            type: 'array',\n            description: `Experimental. Expect unannounced breaking changes (without version bumps).\n\nStops on the trips after this stop. Returned only if \\`fetchStop\\` is \\`true\\` and \\`arriveBy\\` is \\`false\\`.\n`,\n            items: {\n                '$ref': '#/components/schemas/Place'\n            }\n        },\n        pickupDropoffType: {\n            description: 'Type of pickup (for departures) or dropoff (for arrivals), may be disallowed either due to schedule, skipped stops or cancellations',\n            '$ref': '#/components/schemas/PickupDropoffType'\n        },\n        cancelled: {\n            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).',\n            type: 'boolean'\n        },\n        tripCancelled: {\n            description: 'Whether the entire trip is cancelled due to the realtime situation.',\n            type: 'boolean'\n        },\n        source: {\n            description: 'Filename and line number where this trip is from',\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const TripInfoSchema = {\n    description: 'trip id and name',\n    type: 'object',\n    required: ['tripId'],\n    properties: {\n        tripId: {\n            description: 'trip ID (dataset trip id prefixed with the dataset tag)',\n            type: 'string'\n        },\n        routeShortName: {\n            description: 'trip display name (api version < 4)',\n            type: 'string'\n        },\n        displayName: {\n            description: 'trip display name (api version >= 4)',\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const TripSegmentSchema = {\n    description: 'trip segment between two stops to show a trip on a map',\n    type: 'object',\n    required: ['trips', 'mode', 'distance', 'from', 'to', 'departure', 'arrival', 'scheduledArrival', 'scheduledDeparture', 'realTime', 'polyline'],\n    properties: {\n        trips: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/TripInfo'\n            }\n        },\n        routeColor: {\n            type: 'string'\n        },\n        mode: {\n            '$ref': '#/components/schemas/Mode',\n            description: 'Transport mode for this leg'\n        },\n        distance: {\n            type: 'number',\n            description: 'distance in meters'\n        },\n        from: {\n            '$ref': '#/components/schemas/Place'\n        },\n        to: {\n            '$ref': '#/components/schemas/Place'\n        },\n        departure: {\n            description: 'departure time',\n            type: 'string',\n            format: 'date-time'\n        },\n        arrival: {\n            description: 'arrival time',\n            type: 'string',\n            format: 'date-time'\n        },\n        scheduledDeparture: {\n            description: 'scheduled departure time',\n            type: 'string',\n            format: 'date-time'\n        },\n        scheduledArrival: {\n            description: 'scheduled arrival time',\n            type: 'string',\n            format: 'date-time'\n        },\n        realTime: {\n            description: 'Whether there is real-time data about this leg',\n            type: 'boolean'\n        },\n        polyline: {\n            description: 'Google polyline encoded coordinate sequence (with precision 5) where the trip travels on this segment.',\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const DirectionSchema = {\n    type: 'string',\n    enum: ['DEPART', 'HARD_LEFT', 'LEFT', 'SLIGHTLY_LEFT', 'CONTINUE', 'SLIGHTLY_RIGHT', 'RIGHT', 'HARD_RIGHT', 'CIRCLE_CLOCKWISE', 'CIRCLE_COUNTERCLOCKWISE', 'STAIRS', 'ELEVATOR', 'UTURN_LEFT', 'UTURN_RIGHT']\n} as const;\n\nexport const EncodedPolylineSchema = {\n    type: 'object',\n    required: ['points', 'precision', 'length'],\n    properties: {\n        points: {\n            description: 'The encoded points of the polyline using the Google polyline encoding.',\n            type: 'string'\n        },\n        precision: {\n            description: `The precision of the returned polyline (7 for /v1, 6 for /v2)\nBe aware that with precision 7, coordinates with |longitude| > 107.37 are undefined/will overflow.\n`,\n            type: 'integer'\n        },\n        length: {\n            description: 'The number of points in the string',\n            type: 'integer',\n            minimum: 0\n        }\n    }\n} as const;\n\nexport const StepInstructionSchema = {\n    type: 'object',\n    required: ['fromLevel', 'toLevel', 'polyline', 'relativeDirection', 'distance', 'streetName', 'exit', 'stayOn', 'area'],\n    properties: {\n        relativeDirection: {\n            '$ref': '#/components/schemas/Direction'\n        },\n        distance: {\n            description: 'The distance in meters that this step takes.',\n            type: 'number'\n        },\n        fromLevel: {\n            description: 'level where this segment starts, based on OpenStreetMap data',\n            type: 'number'\n        },\n        toLevel: {\n            description: 'level where this segment starts, based on OpenStreetMap data',\n            type: 'number'\n        },\n        osmWay: {\n            description: 'OpenStreetMap way index',\n            type: 'integer'\n        },\n        polyline: {\n            '$ref': '#/components/schemas/EncodedPolyline'\n        },\n        streetName: {\n            description: 'The name of the street.',\n            type: 'string'\n        },\n        exit: {\n            description: `Not implemented!\nWhen exiting a highway or traffic circle, the exit name/number.\n`,\n            type: 'string'\n        },\n        stayOn: {\n            description: `Not implemented!\nIndicates whether or not a street changes direction at an intersection.\n`,\n            type: 'boolean'\n        },\n        area: {\n            description: `Not implemented!\nThis step is on an open area, such as a plaza or train platform,\nand thus the directions should say something like \"cross\"\n`,\n            type: 'boolean'\n        },\n        toll: {\n            description: 'Indicates that a fee must be paid by general traffic to use a road, road bridge or road tunnel.',\n            type: 'boolean'\n        },\n        accessRestriction: {\n            description: `Experimental. Indicates whether access to this part of the route is restricted.\nSee: https://wiki.openstreetmap.org/wiki/Conditional_restrictions\n`,\n            type: 'string'\n        },\n        elevationUp: {\n            type: 'integer',\n            description: 'incline in meters across this path segment'\n        },\n        elevationDown: {\n            type: 'integer',\n            description: 'decline in meters across this path segment'\n        }\n    }\n} as const;\n\nexport const RentalFormFactorSchema = {\n    type: 'string',\n    enum: ['BICYCLE', 'CARGO_BICYCLE', 'CAR', 'MOPED', 'SCOOTER_STANDING', 'SCOOTER_SEATED', 'OTHER']\n} as const;\n\nexport const RentalPropulsionTypeSchema = {\n    type: 'string',\n    enum: ['HUMAN', 'ELECTRIC_ASSIST', 'ELECTRIC', 'COMBUSTION', 'COMBUSTION_DIESEL', 'HYBRID', 'PLUG_IN_HYBRID', 'HYDROGEN_FUEL_CELL']\n} as const;\n\nexport const RentalReturnConstraintSchema = {\n    type: 'string',\n    enum: ['NONE', 'ANY_STATION', 'ROUNDTRIP_STATION']\n} as const;\n\nexport const RentalSchema = {\n    description: 'Vehicle rental',\n    type: 'object',\n    required: ['providerId', 'providerGroupId', 'systemId'],\n    properties: {\n        providerId: {\n            type: 'string',\n            description: 'Rental provider ID'\n        },\n        providerGroupId: {\n            type: 'string',\n            description: 'Rental provider group ID'\n        },\n        systemId: {\n            type: 'string',\n            description: 'Vehicle share system ID'\n        },\n        systemName: {\n            type: 'string',\n            description: 'Vehicle share system name'\n        },\n        url: {\n            type: 'string',\n            description: 'URL of the vehicle share system'\n        },\n        color: {\n            type: 'string',\n            description: `Color associated with this provider, in hexadecimal RGB format\n(e.g. \"#FF0000\" for red). Can be empty.\n`\n        },\n        stationName: {\n            type: 'string',\n            description: 'Name of the station'\n        },\n        fromStationName: {\n            type: 'string',\n            description: 'Name of the station where the vehicle is picked up (empty for free floating vehicles)'\n        },\n        toStationName: {\n            type: 'string',\n            description: 'Name of the station where the vehicle is returned (empty for free floating vehicles)'\n        },\n        rentalUriAndroid: {\n            type: 'string',\n            description: 'Rental URI for Android (deep link to the specific station or vehicle)'\n        },\n        rentalUriIOS: {\n            type: 'string',\n            description: 'Rental URI for iOS (deep link to the specific station or vehicle)'\n        },\n        rentalUriWeb: {\n            type: 'string',\n            description: 'Rental URI for web (deep link to the specific station or vehicle)'\n        },\n        formFactor: {\n            '$ref': '#/components/schemas/RentalFormFactor'\n        },\n        propulsionType: {\n            '$ref': '#/components/schemas/RentalPropulsionType'\n        },\n        returnConstraint: {\n            '$ref': '#/components/schemas/RentalReturnConstraint'\n        }\n    }\n} as const;\n\nexport const MultiPolygonSchema = {\n    type: 'array',\n    description: `A multi-polygon contains a number of polygons, each containing a number\nof rings, which are encoded as polylines (with precision 6).\n\nFor each polygon, the first ring is the outer ring, all subsequent rings\nare inner rings (holes).\n`,\n    items: {\n        type: 'array',\n        items: {\n            '$ref': '#/components/schemas/EncodedPolyline'\n        }\n    }\n} as const;\n\nexport const RentalZoneRestrictionsSchema = {\n    type: 'object',\n    required: ['vehicleTypeIdxs', 'rideStartAllowed', 'rideEndAllowed', 'rideThroughAllowed'],\n    properties: {\n        vehicleTypeIdxs: {\n            type: 'array',\n            description: `List of vehicle types (as indices into the provider's vehicle types\narray) to which these restrictions apply.\nIf empty, the restrictions apply to all vehicle types of the provider.\n`,\n            items: {\n                type: 'integer'\n            }\n        },\n        rideStartAllowed: {\n            type: 'boolean',\n            description: 'whether the ride is allowed to start in this zone'\n        },\n        rideEndAllowed: {\n            type: 'boolean',\n            description: 'whether the ride is allowed to end in this zone'\n        },\n        rideThroughAllowed: {\n            type: 'boolean',\n            description: 'whether the ride is allowed to pass through this zone'\n        },\n        stationParking: {\n            type: 'boolean',\n            description: 'whether vehicles can only be parked at stations in this zone'\n        }\n    }\n} as const;\n\nexport const RentalVehicleTypeSchema = {\n    type: 'object',\n    required: ['id', 'formFactor', 'propulsionType', 'returnConstraint', 'returnConstraintGuessed'],\n    properties: {\n        id: {\n            type: 'string',\n            description: 'Unique identifier of the vehicle type (unique within the provider)'\n        },\n        name: {\n            type: 'string',\n            description: 'Public name of the vehicle type (can be empty)'\n        },\n        formFactor: {\n            '$ref': '#/components/schemas/RentalFormFactor'\n        },\n        propulsionType: {\n            '$ref': '#/components/schemas/RentalPropulsionType'\n        },\n        returnConstraint: {\n            '$ref': '#/components/schemas/RentalReturnConstraint'\n        },\n        returnConstraintGuessed: {\n            type: 'boolean',\n            description: 'Whether the return constraint was guessed instead of being specified by the rental provider'\n        }\n    }\n} as const;\n\nexport const RentalProviderSchema = {\n    type: 'object',\n    required: ['id', 'name', 'groupId', 'bbox', 'vehicleTypes', 'formFactors', 'defaultRestrictions', 'globalGeofencingRules'],\n    properties: {\n        id: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider'\n        },\n        name: {\n            type: 'string',\n            description: 'Name of the provider to be displayed to customers'\n        },\n        groupId: {\n            type: 'string',\n            description: 'Id of the rental provider group this provider belongs to'\n        },\n        operator: {\n            type: 'string',\n            description: 'Name of the system operator'\n        },\n        url: {\n            type: 'string',\n            description: 'URL of the vehicle share system'\n        },\n        purchaseUrl: {\n            type: 'string',\n            description: 'URL where a customer can purchase a membership'\n        },\n        color: {\n            type: 'string',\n            description: `Color associated with this provider, in hexadecimal RGB format\n(e.g. \"#FF0000\" for red). Can be empty.\n`\n        },\n        bbox: {\n            type: 'array',\n            description: `Bounding box of the area covered by this rental provider,\n[west, south, east, north] as [lon, lat, lon, lat]\n`,\n            minItems: 4,\n            maxItems: 4,\n            items: {\n                type: 'number'\n            }\n        },\n        vehicleTypes: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/RentalVehicleType'\n            }\n        },\n        formFactors: {\n            type: 'array',\n            description: 'List of form factors offered by this provider',\n            items: {\n                '$ref': '#/components/schemas/RentalFormFactor'\n            }\n        },\n        defaultRestrictions: {\n            '$ref': '#/components/schemas/RentalZoneRestrictions'\n        },\n        globalGeofencingRules: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/RentalZoneRestrictions'\n            }\n        }\n    }\n} as const;\n\nexport const RentalProviderGroupSchema = {\n    type: 'object',\n    required: ['id', 'name', 'providers', 'formFactors'],\n    properties: {\n        id: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider group'\n        },\n        name: {\n            type: 'string',\n            description: 'Name of the provider group to be displayed to customers'\n        },\n        color: {\n            type: 'string',\n            description: `Color associated with this provider group, in hexadecimal RGB format\n(e.g. \"#FF0000\" for red). Can be empty.\n`\n        },\n        providers: {\n            type: 'array',\n            description: 'List of rental provider IDs that belong to this group',\n            items: {\n                type: 'string'\n            }\n        },\n        formFactors: {\n            type: 'array',\n            description: 'List of form factors offered by this provider group',\n            items: {\n                '$ref': '#/components/schemas/RentalFormFactor'\n            }\n        }\n    }\n} as const;\n\nexport const RentalStationSchema = {\n    type: 'object',\n    required: ['id', 'providerId', 'providerGroupId', 'name', 'lat', 'lon', 'isRenting', 'isReturning', 'numVehiclesAvailable', 'formFactors', 'vehicleTypesAvailable', 'vehicleDocksAvailable', 'bbox'],\n    properties: {\n        id: {\n            type: 'string',\n            description: 'Unique identifier of the rental station'\n        },\n        providerId: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider'\n        },\n        providerGroupId: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider group'\n        },\n        name: {\n            type: 'string',\n            description: 'Public name of the station'\n        },\n        lat: {\n            description: 'latitude',\n            type: 'number'\n        },\n        lon: {\n            description: 'longitude',\n            type: 'number'\n        },\n        address: {\n            type: 'string',\n            description: 'Address where the station is located'\n        },\n        crossStreet: {\n            type: 'string',\n            description: 'Cross street or landmark where the station is located'\n        },\n        rentalUriAndroid: {\n            type: 'string',\n            description: 'Rental URI for Android (deep link to the specific station)'\n        },\n        rentalUriIOS: {\n            type: 'string',\n            description: 'Rental URI for iOS (deep link to the specific station)'\n        },\n        rentalUriWeb: {\n            type: 'string',\n            description: 'Rental URI for web (deep link to the specific station)'\n        },\n        isRenting: {\n            type: 'boolean',\n            description: 'true if vehicles can be rented from this station, false if it is temporarily out of service'\n        },\n        isReturning: {\n            type: 'boolean',\n            description: 'true if vehicles can be returned to this station, false if it is temporarily out of service'\n        },\n        numVehiclesAvailable: {\n            type: 'integer',\n            description: 'Number of vehicles available for rental at this station'\n        },\n        formFactors: {\n            type: 'array',\n            description: 'List of form factors available for rental and/or return at this station',\n            items: {\n                '$ref': '#/components/schemas/RentalFormFactor'\n            }\n        },\n        vehicleTypesAvailable: {\n            type: 'object',\n            description: 'List of vehicle types currently available at this station (vehicle type ID -> count)',\n            additionalProperties: {\n                type: 'integer'\n            }\n        },\n        vehicleDocksAvailable: {\n            type: 'object',\n            description: 'List of vehicle docks currently available at this station (vehicle type ID -> count)',\n            additionalProperties: {\n                type: 'integer'\n            }\n        },\n        stationArea: {\n            '$ref': '#/components/schemas/MultiPolygon'\n        },\n        bbox: {\n            type: 'array',\n            description: `Bounding box of the area covered by this station,\n[west, south, east, north] as [lon, lat, lon, lat]\n`,\n            minItems: 4,\n            maxItems: 4,\n            items: {\n                type: 'number'\n            }\n        }\n    }\n} as const;\n\nexport const RentalVehicleSchema = {\n    type: 'object',\n    required: ['id', 'providerId', 'providerGroupId', 'typeId', 'lat', 'lon', 'formFactor', 'propulsionType', 'returnConstraint', 'isReserved', 'isDisabled'],\n    properties: {\n        id: {\n            type: 'string',\n            description: 'Unique identifier of the rental vehicle'\n        },\n        providerId: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider'\n        },\n        providerGroupId: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider group'\n        },\n        typeId: {\n            type: 'string',\n            description: 'Vehicle type ID (unique within the provider)'\n        },\n        lat: {\n            description: 'latitude',\n            type: 'number'\n        },\n        lon: {\n            description: 'longitude',\n            type: 'number'\n        },\n        formFactor: {\n            '$ref': '#/components/schemas/RentalFormFactor'\n        },\n        propulsionType: {\n            '$ref': '#/components/schemas/RentalPropulsionType'\n        },\n        returnConstraint: {\n            '$ref': '#/components/schemas/RentalReturnConstraint'\n        },\n        stationId: {\n            type: 'string',\n            description: 'Station ID if the vehicle is currently at a station'\n        },\n        homeStationId: {\n            type: 'string',\n            description: 'Station ID where the vehicle must be returned, if applicable'\n        },\n        isReserved: {\n            type: 'boolean',\n            description: 'true if the vehicle is currently reserved by a customer, false otherwise'\n        },\n        isDisabled: {\n            type: 'boolean',\n            description: 'true if the vehicle is out of service, false otherwise'\n        },\n        rentalUriAndroid: {\n            type: 'string',\n            description: 'Rental URI for Android (deep link to the specific vehicle)'\n        },\n        rentalUriIOS: {\n            type: 'string',\n            description: 'Rental URI for iOS (deep link to the specific vehicle)'\n        },\n        rentalUriWeb: {\n            type: 'string',\n            description: 'Rental URI for web (deep link to the specific vehicle)'\n        }\n    }\n} as const;\n\nexport const RentalZoneSchema = {\n    type: 'object',\n    required: ['providerId', 'providerGroupId', 'z', 'bbox', 'area', 'rules'],\n    properties: {\n        providerId: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider'\n        },\n        providerGroupId: {\n            type: 'string',\n            description: 'Unique identifier of the rental provider group'\n        },\n        name: {\n            type: 'string',\n            description: 'Public name of the geofencing zone'\n        },\n        z: {\n            type: 'integer',\n            description: 'Zone precedence / z-index (higher number = higher precedence)'\n        },\n        bbox: {\n            type: 'array',\n            description: `Bounding box of the area covered by this zone,\n[west, south, east, north] as [lon, lat, lon, lat]\n`,\n            minItems: 4,\n            maxItems: 4,\n            items: {\n                type: 'number'\n            }\n        },\n        area: {\n            '$ref': '#/components/schemas/MultiPolygon'\n        },\n        rules: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/RentalZoneRestrictions'\n            }\n        }\n    }\n} as const;\n\nexport const CategorySchema = {\n    type: 'object',\n    required: ['id', 'name', 'shortName'],\n    description: `not available for GTFS datasets by default\nFor NeTEx it contains information about the vehicle category, e.g. IC/InterCity\n`,\n    properties: {\n        id: {\n            type: 'string'\n        },\n        name: {\n            type: 'string'\n        },\n        shortName: {\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const LegSchema = {\n    type: 'object',\n    required: ['mode', 'startTime', 'endTime', 'scheduledStartTime', 'scheduledEndTime', 'realTime', 'scheduled', 'duration', 'from', 'to', 'legGeometry'],\n    properties: {\n        mode: {\n            '$ref': '#/components/schemas/Mode',\n            description: 'Transport mode for this leg'\n        },\n        from: {\n            '$ref': '#/components/schemas/Place'\n        },\n        to: {\n            '$ref': '#/components/schemas/Place'\n        },\n        duration: {\n            description: `Leg duration in seconds\n\nIf leg is footpath:\n  The footpath duration is derived from the default footpath\n  duration using the query parameters \\`transferTimeFactor\\` and\n  \\`additionalTransferTime\\` as follows:\n  \\`leg.duration = defaultDuration * transferTimeFactor + additionalTransferTime.\\`\n  In case the defaultDuration is needed, it can be calculated by\n  \\`defaultDuration = (leg.duration - additionalTransferTime) / transferTimeFactor\\`.\n  Note that the default values are \\`transferTimeFactor = 1\\` and\n  \\`additionalTransferTime = 0\\` in case they are not explicitly\n  provided in the query.\n`,\n            type: 'integer'\n        },\n        startTime: {\n            type: 'string',\n            format: 'date-time',\n            description: 'leg departure time'\n        },\n        endTime: {\n            type: 'string',\n            format: 'date-time',\n            description: 'leg arrival time'\n        },\n        scheduledStartTime: {\n            type: 'string',\n            format: 'date-time',\n            description: 'scheduled leg departure time'\n        },\n        scheduledEndTime: {\n            type: 'string',\n            format: 'date-time',\n            description: 'scheduled leg arrival time'\n        },\n        realTime: {\n            description: 'Whether there is real-time data about this leg',\n            type: 'boolean'\n        },\n        scheduled: {\n            description: `Whether this leg was originally scheduled to run or is an additional service.\nScheduled times will equal realtime times in this case.\n`,\n            type: 'boolean'\n        },\n        distance: {\n            description: 'For non-transit legs the distance traveled while traversing this leg in meters.',\n            type: 'number'\n        },\n        interlineWithPreviousLeg: {\n            description: 'For transit legs, if the rider should stay on the vehicle as it changes route names.',\n            type: 'boolean'\n        },\n        headsign: {\n            description: `For transit legs, the headsign of the bus or train being used.\nFor non-transit legs, null\n`,\n            type: 'string'\n        },\n        tripFrom: {\n            description: 'first stop of this trip',\n            '$ref': '#/components/schemas/Place'\n        },\n        tripTo: {\n            description: 'final stop of this trip (can differ from headsign)',\n            '$ref': '#/components/schemas/Place'\n        },\n        category: {\n            '$ref': '#/components/schemas/Category'\n        },\n        routeId: {\n            type: 'string'\n        },\n        routeUrl: {\n            type: 'string'\n        },\n        directionId: {\n            type: 'string'\n        },\n        routeColor: {\n            type: 'string'\n        },\n        routeTextColor: {\n            type: 'string'\n        },\n        routeType: {\n            type: 'integer'\n        },\n        agencyName: {\n            type: 'string'\n        },\n        agencyUrl: {\n            type: 'string'\n        },\n        agencyId: {\n            type: 'string'\n        },\n        tripId: {\n            type: 'string'\n        },\n        routeShortName: {\n            type: 'string'\n        },\n        routeLongName: {\n            type: 'string'\n        },\n        tripShortName: {\n            type: 'string'\n        },\n        displayName: {\n            type: 'string'\n        },\n        cancelled: {\n            description: 'Whether this trip is cancelled',\n            type: 'boolean'\n        },\n        source: {\n            description: 'Filename and line number where this trip is from',\n            type: 'string'\n        },\n        intermediateStops: {\n            description: `For transit legs, intermediate stops between the Place where the leg originates\nand the Place where the leg ends. For non-transit legs, null.\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Place'\n            }\n        },\n        legGeometry: {\n            description: `Encoded geometry of the leg.\nIf detailed leg output is disabled, this is returned as an empty\npolyline.\n`,\n            '$ref': '#/components/schemas/EncodedPolyline'\n        },\n        steps: {\n            description: `A series of turn by turn instructions\nused for walking, biking and driving.\nThis field is omitted if the request disables detailed leg output.\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/StepInstruction'\n            }\n        },\n        rental: {\n            '$ref': '#/components/schemas/Rental'\n        },\n        fareTransferIndex: {\n            type: 'integer',\n            description: `Index into \\`Itinerary.fareTransfers\\` array\nto identify which fare transfer this leg belongs to\n`\n        },\n        effectiveFareLegIndex: {\n            type: 'integer',\n            description: `Index into the \\`Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts\\` array\nto identify which effective fare leg this itinerary leg belongs to\n`\n        },\n        alerts: {\n            description: 'Alerts for this stop.',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Alert'\n            }\n        },\n        loopedCalendarSince: {\n            description: `If set, this attribute indicates that this trip has been expanded\nbeyond the feed end date (enabled by config flag \\`timetable.dataset.extend_calendar\\`)\nby looping active weekdays, e.g. from calendar.txt in GTFS.\n`,\n            type: 'string',\n            format: 'date-time'\n        },\n        bikesAllowed: {\n            description: `Whether bikes can be carried on this leg.\n`,\n            type: 'boolean'\n        }\n    }\n} as const;\n\nexport const RiderCategorySchema = {\n    type: 'object',\n    required: ['riderCategoryName', 'isDefaultFareCategory'],\n    properties: {\n        riderCategoryName: {\n            description: 'Rider category name as displayed to the rider.',\n            type: 'string'\n        },\n        isDefaultFareCategory: {\n            description: 'Specifies if this category should be considered the default (i.e. the main category displayed to riders).',\n            type: 'boolean'\n        },\n        eligibilityUrl: {\n            description: 'URL to a web page providing detailed information about the rider category and/or its eligibility criteria.',\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const FareMediaTypeSchema = {\n    type: 'string',\n    enum: ['NONE', 'PAPER_TICKET', 'TRANSIT_CARD', 'CONTACTLESS_EMV', 'MOBILE_APP'],\n    description: `- \\`NONE\\`: No fare media involved (e.g., cash payment)\n- \\`PAPER_TICKET\\`: Physical paper ticket\n- \\`TRANSIT_CARD\\`: Physical transit card with stored value\n- \\`CONTACTLESS_EMV\\`: cEMV (contactless payment)\n- \\`MOBILE_APP\\`: Mobile app with virtual transit cards/passes\n`\n} as const;\n\nexport const FareMediaSchema = {\n    type: 'object',\n    required: ['fareMediaType'],\n    properties: {\n        fareMediaName: {\n            description: 'Name of the fare media. Required for transit cards and mobile apps.',\n            type: 'string'\n        },\n        fareMediaType: {\n            description: 'The type of fare media.',\n            '$ref': '#/components/schemas/FareMediaType'\n        }\n    }\n} as const;\n\nexport const FareProductSchema = {\n    type: 'object',\n    required: ['name', 'amount', 'currency'],\n    properties: {\n        name: {\n            description: 'The name of the fare product as displayed to riders.',\n            type: 'string'\n        },\n        amount: {\n            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.',\n            type: 'number'\n        },\n        currency: {\n            description: 'ISO 4217 currency code. The currency of the cost of the fare product.',\n            type: 'string'\n        },\n        riderCategory: {\n            '$ref': '#/components/schemas/RiderCategory'\n        },\n        media: {\n            '$ref': '#/components/schemas/FareMedia'\n        }\n    }\n} as const;\n\nexport const FareTransferRuleSchema = {\n    type: 'string',\n    enum: ['A_AB', 'A_AB_B', 'AB']\n} as const;\n\nexport const FareTransferSchema = {\n    type: 'object',\n    description: `The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt\n\nTerminology:\n  - **Leg**: An itinerary leg as described by the \\`Leg\\` type of this API description.\n  - **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*.\n  - **Fare Transfer**: A fare transfer groups two or more effective fare legs.\n  - **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer\n  - **B** is any *effective fare leg* following the first *effective fare leg* in this transfer\n  - **AB** are all changes between *effective fare legs* contained in this transfer\n\nThe fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer:\n  - 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.\n  - 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)\n  - 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.\n\nAn itinerary \\`Leg\\` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to.\n`,\n    required: ['effectiveFareLegProducts'],\n    properties: {\n        rule: {\n            '$ref': '#/components/schemas/FareTransferRule'\n        },\n        transferProducts: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/FareProduct'\n            }\n        },\n        effectiveFareLegProducts: {\n            description: `Lists all valid fare products for the effective fare legs.\nThis is an \\`array<array<FareProduct>>\\` where the inner array\nlists all possible fare products that would cover this effective fare leg.\nEach \"effective fare leg\" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc.\nYou 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\\`)\nand the inner array as OR (you can choose which ticket to buy)\n`,\n            type: 'array',\n            items: {\n                type: 'array',\n                items: {\n                    type: 'array',\n                    items: {\n                        '$ref': '#/components/schemas/FareProduct'\n                    }\n                }\n            }\n        }\n    }\n} as const;\n\nexport const ItinerarySchema = {\n    type: 'object',\n    required: ['duration', 'startTime', 'endTime', 'transfers', 'legs'],\n    properties: {\n        duration: {\n            description: 'journey duration in seconds',\n            type: 'integer'\n        },\n        startTime: {\n            type: 'string',\n            format: 'date-time',\n            description: 'journey departure time'\n        },\n        endTime: {\n            type: 'string',\n            format: 'date-time',\n            description: 'journey arrival time'\n        },\n        transfers: {\n            type: 'integer',\n            description: 'The number of transfers this trip has.'\n        },\n        legs: {\n            description: 'Journey legs',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Leg'\n            }\n        },\n        fareTransfers: {\n            description: 'Fare information',\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/FareTransfer'\n            }\n        }\n    }\n} as const;\n\nexport const TransferSchema = {\n    description: 'transfer from one location to another',\n    type: 'object',\n    required: ['to'],\n    properties: {\n        to: {\n            '$ref': '#/components/schemas/Place'\n        },\n        default: {\n            type: 'number',\n            description: `optional; missing if the GTFS did not contain a transfer\ntransfer duration in minutes according to GTFS (+heuristics)\n`\n        },\n        foot: {\n            type: 'number',\n            description: `optional; missing if no path was found (timetable / osr)\ntransfer duration in minutes for the foot profile\n`\n        },\n        footRouted: {\n            type: 'number',\n            description: `optional; missing if no path was found with foot routing\ntransfer duration in minutes for the foot profile\n`\n        },\n        wheelchair: {\n            type: 'number',\n            description: `optional; missing if no path was found with the wheelchair profile \ntransfer duration in minutes for the wheelchair profile\n`\n        },\n        wheelchairRouted: {\n            type: 'number',\n            description: `optional; missing if no path was found with the wheelchair profile\ntransfer duration in minutes for the wheelchair profile\n`\n        },\n        wheelchairUsesElevator: {\n            type: 'boolean',\n            description: `optional; missing if no path was found with the wheelchair profile\ntrue if the wheelchair path uses an elevator\n`\n        },\n        car: {\n            type: 'number',\n            description: `optional; missing if no path was found with car routing\ntransfer duration in minutes for the car profile\n`\n        }\n    }\n} as const;\n\nexport const OneToManyParamsSchema = {\n    type: 'object',\n    required: ['one', 'many', 'mode', 'max', 'maxMatchingDistance', 'arriveBy'],\n    properties: {\n        one: {\n            description: 'geo location as latitude;longitude',\n            type: 'string'\n        },\n        many: {\n            description: `geo locations as latitude;longitude,latitude;longitude,...\n\nThe number of accepted locations is limited by server config variable \\`onetomany_max_many\\`.\n`,\n            type: 'array',\n            items: {\n                type: 'string'\n            },\n            explode: false\n        },\n        mode: {\n            description: `routing profile to use (currently supported: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`)\n`,\n            '$ref': '#/components/schemas/Mode'\n        },\n        max: {\n            description: 'maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`.',\n            type: 'number'\n        },\n        maxMatchingDistance: {\n            description: 'maximum matching distance in meters to match geo coordinates to the street network',\n            type: 'number'\n        },\n        elevationCosts: {\n            description: `Optional. Default is \\`NONE\\`.\n\nSet an elevation cost profile, to penalize routes with incline.\n- \\`NONE\\`: No additional costs for elevations. This is the default behavior\n- \\`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.\n- \\`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.\n\nAs using an elevation costs profile will increase the travel duration,\nrouting through steep terrain may exceed the maximal allowed duration,\ncausing a location to appear unreachable.\nIncreasing the maximum travel time for these segments may resolve this issue.\n\nElevation cost profiles are currently used by following street modes:\n- \\`BIKE\\`\n`,\n            '$ref': '#/components/schemas/ElevationCosts',\n            default: 'NONE'\n        },\n        arriveBy: {\n            description: `true = many to one\nfalse = one to many\n`,\n            type: 'boolean'\n        },\n        withDistance: {\n            description: `If true, the response includes the distance in meters\nfor each path. This requires path reconstruction and\nmay be slower than duration-only queries.\n`,\n            type: 'boolean',\n            default: false\n        }\n    }\n} as const;\n\nexport const OneToManyIntermodalParamsSchema = {\n    type: 'object',\n    required: ['one', 'many'],\n    properties: {\n        one: {\n            description: `\\`latitude,longitude[,level]\\` tuple with\n- latitude and longitude in degrees\n- (optional) level: the OSM level (default: 0)\n\nOR\n\nstop id\n`,\n            type: 'string'\n        },\n        many: {\n            description: `array of:\n\n\\`latitude,longitude[,level]\\` tuple with\n- latitude and longitude in degrees\n- (optional) level: the OSM level (default: 0)\n\nOR\n\nstop id\n\nThe number of accepted locations is limited by server config variable \\`onetomany_max_many\\`.\n`,\n            type: 'array',\n            items: {\n                type: 'string'\n            },\n            explode: false\n        },\n        time: {\n            description: `Optional. Defaults to the current time.\n\nDeparture time ($arriveBy=false) / arrival date ($arriveBy=true),\n`,\n            type: 'string',\n            format: 'date-time'\n        },\n        maxTravelTime: {\n            description: `The maximum travel time in minutes.\nIf not provided, the routing uses the value\nhardcoded in the server which is usually quite high.\n\n*Warning*: Use with care. Setting this too low can lead to\noptimal (e.g. the least transfers) journeys not being found.\nIf this value is too low to reach the destination at all,\nit can lead to slow routing performance.\n`,\n            type: 'integer'\n        },\n        maxMatchingDistance: {\n            description: 'maximum matching distance in meters to match geo coordinates to the street network',\n            type: 'number',\n            default: 25\n        },\n        arriveBy: {\n            description: `Optional. Defaults to false, i.e. one to many search\n\ntrue = many to one\nfalse = one to many\n`,\n            type: 'boolean',\n            default: false\n        },\n        maxTransfers: {\n            description: `The maximum number of allowed transfers (i.e. interchanges between transit legs,\npre- and postTransit do not count as transfers).\n\\`maxTransfers=0\\` searches for direct transit connections without any transfers.\nIf you want to search only for non-transit connections (\\`FOOT\\`, \\`CAR\\`, etc.),\nsend an empty \\`transitModes\\` parameter instead.\n\nIf not provided, the routing uses the server-side default value\nwhich is hardcoded and very high to cover all use cases.\n\n*Warning*: Use with care. Setting this too low can lead to\noptimal (e.g. the fastest) journeys not being found.\nIf this value is too low to reach the destination at all,\nit can lead to slow routing performance.\n`,\n            type: 'integer'\n        },\n        minTransferTime: {\n            description: `Optional. Default is 0 minutes.\n\nMinimum transfer time for each transfer in minutes.\n`,\n            type: 'integer',\n            default: 0\n        },\n        additionalTransferTime: {\n            description: `Optional. Default is 0 minutes.\n\nAdditional transfer time reserved for each transfer in minutes.\n`,\n            type: 'integer',\n            default: 0\n        },\n        transferTimeFactor: {\n            description: `Optional. Default is 1.0\n\nFactor to multiply minimum required transfer times with.\nValues smaller than 1.0 are not supported.\n`,\n            type: 'number',\n            default: 1\n        },\n        useRoutedTransfers: {\n            description: `Optional. Default is \\`false\\`.\n\nWhether to use transfers routed on OpenStreetMap data.\n`,\n            type: 'boolean',\n            default: false\n        },\n        pedestrianProfile: {\n            description: `Optional. Default is \\`FOOT\\`.\n\nAccessibility profile to use for pedestrian routing in transfers\nbetween transit connections and the first and last mile respectively.\n`,\n            '$ref': '#/components/schemas/PedestrianProfile',\n            default: 'FOOT'\n        },\n        pedestrianSpeed: {\n            description: `Optional\n\nAverage speed for pedestrian routing.\n`,\n            '$ref': '#/components/schemas/PedestrianSpeed'\n        },\n        cyclingSpeed: {\n            description: `Optional\n\nAverage speed for bike routing.\n`,\n            '$ref': '#/components/schemas/CyclingSpeed'\n        },\n        elevationCosts: {\n            description: `Optional. Default is \\`NONE\\`.\n\nSet an elevation cost profile, to penalize routes with incline.\n- \\`NONE\\`: No additional costs for elevations. This is the default behavior\n- \\`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.\n- \\`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.\n\nAs using an elevation costs profile will increase the travel duration,\nrouting through steep terrain may exceed the maximal allowed duration,\ncausing a location to appear unreachable.\nIncreasing the maximum travel time for these segments may resolve this issue.\n\nThe profile is used for routing on both the first and last mile.\n\nElevation cost profiles are currently used by following street modes:\n- \\`BIKE\\`\n`,\n            '$ref': '#/components/schemas/ElevationCosts',\n            default: 'NONE'\n        },\n        transitModes: {\n            description: `Optional. Default is \\`TRANSIT\\` which allows all transit modes (no restriction).\nAllowed modes for the transit part. If empty, no transit connections will be computed.\nFor example, this can be used to allow only \\`SUBURBAN,SUBWAY,TRAM\\`.\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Mode'\n            },\n            default: ['TRANSIT'],\n            explode: false\n        },\n        preTransitModes: {\n            description: `Optional. Default is \\`WALK\\`. Does not apply to direct connections (see \\`directMode\\`).\n\nA 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\\`.\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Mode'\n            },\n            default: ['WALK'],\n            explode: false\n        },\n        postTransitModes: {\n            description: `Optional. Default is \\`WALK\\`. Does not apply to direct connections (see \\`directMode\\`).\n\nA 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\\`.\n`,\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/Mode'\n            },\n            default: ['WALK'],\n            explode: false\n        },\n        directMode: {\n            description: `Default is \\`WALK\\` which will compute walking routes as direct connections.\n\nMode used for direction connections from start to destination without using transit.\n\nCurrently supported non-transit modes: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`\n`,\n            '$ref': '#/components/schemas/Mode',\n            default: 'WALK'\n        },\n        maxPreTransitTime: {\n            description: `Optional. Default is 15min which is \\`900\\`.\nMaximum time in seconds for the first street leg.\nIs limited by server config variable \\`street_routing_max_prepost_transit_seconds\\`.\n`,\n            type: 'integer',\n            default: 900,\n            minimum: 0\n        },\n        maxPostTransitTime: {\n            description: `Optional. Default is 15min which is \\`900\\`.\nMaximum time in seconds for the last street leg.\nIs limited by server config variable \\`street_routing_max_prepost_transit_seconds\\`.\n`,\n            type: 'integer',\n            default: 900,\n            minimum: 0\n        },\n        maxDirectTime: {\n            description: `Optional. Default is 30min which is \\`1800\\`.\nMaximum time in seconds for direct connections.\n\nIf a value smaller than either \\`maxPreTransitTime\\` or\n\\`maxPostTransitTime\\` is used, their maximum is set instead.\nIs limited by server config variable \\`street_routing_max_direct_seconds\\`.\n`,\n            type: 'integer',\n            default: 1800,\n            minimum: 0\n        },\n        withDistance: {\n            description: `If true, the response includes the distance in meters\nfor each path. This requires path reconstruction and\nmay be slower than duration-only queries.\n\n\\`withDistance\\` is currently limited to street routing.\n`,\n            type: 'boolean',\n            default: false\n        },\n        requireBikeTransport: {\n            description: `Optional. Default is \\`false\\`.\n\nIf set to \\`true\\`, all used transit trips are required to allow bike carriage.\n`,\n            type: 'boolean',\n            default: false\n        },\n        requireCarTransport: {\n            description: `Optional. Default is \\`false\\`.\n\nIf set to \\`true\\`, all used transit trips are required to allow car carriage.\n`,\n            type: 'boolean',\n            default: false\n        }\n    }\n} as const;\n\nexport const ServerConfigSchema = {\n    Description: 'server configuration',\n    type: 'object',\n    required: ['motisVersion', 'hasElevation', 'hasRoutedTransfers', 'hasStreetRouting', 'maxOneToManySize', 'maxOneToAllTravelTimeLimit', 'maxPrePostTransitTimeLimit', 'maxDirectTimeLimit', 'shapesDebugEnabled'],\n    properties: {\n        motisVersion: {\n            description: 'the version of this MOTIS server',\n            type: 'string'\n        },\n        hasElevation: {\n            description: 'true if elevation is loaded',\n            type: 'boolean'\n        },\n        hasRoutedTransfers: {\n            description: 'true if routed transfers available',\n            type: 'boolean'\n        },\n        hasStreetRouting: {\n            description: 'true if street routing is available',\n            type: 'boolean'\n        },\n        maxOneToManySize: {\n            description: `limit for the number of \\`many\\` locations for one-to-many requests\n`,\n            type: 'number'\n        },\n        maxOneToAllTravelTimeLimit: {\n            description: 'limit for maxTravelTime API param in minutes',\n            type: 'number'\n        },\n        maxPrePostTransitTimeLimit: {\n            description: 'limit for maxPrePostTransitTime API param in seconds',\n            type: 'number'\n        },\n        maxDirectTimeLimit: {\n            description: 'limit for maxDirectTime API param in seconds',\n            type: 'number'\n        },\n        shapesDebugEnabled: {\n            description: 'true if experimental route shapes debug download API is enabled',\n            type: 'boolean'\n        }\n    }\n} as const;\n\nexport const ErrorSchema = {\n    type: 'object',\n    required: ['error'],\n    properties: {\n        error: {\n            type: 'string',\n            description: 'error message'\n        }\n    }\n} as const;\n\nexport const RouteSegmentSchema = {\n    description: 'Route segment between two stops to show a route on a map',\n    type: 'object',\n    required: ['from', 'to', 'polyline'],\n    properties: {\n        from: {\n            type: 'integer',\n            description: 'Index into the top-level route stops array'\n        },\n        to: {\n            type: 'integer',\n            description: 'Index into the top-level route stops array'\n        },\n        polyline: {\n            type: 'integer',\n            description: 'Index into the top-level route polylines array'\n        }\n    }\n} as const;\n\nexport const RoutePolylineSchema = {\n    description: 'Shared polyline used by one or more route segments',\n    type: 'object',\n    required: ['polyline', 'colors', 'routeIndexes'],\n    properties: {\n        polyline: {\n            '$ref': '#/components/schemas/EncodedPolyline'\n        },\n        colors: {\n            type: 'array',\n            description: 'Unique route colors of routes containing this segment',\n            items: {\n                type: 'string'\n            }\n        },\n        routeIndexes: {\n            type: 'array',\n            description: 'Indexes into the top-level routes array for routes containing this segment',\n            items: {\n                type: 'integer'\n            }\n        }\n    }\n} as const;\n\nexport const RouteColorSchema = {\n    type: 'object',\n    required: ['color', 'textColor'],\n    properties: {\n        color: {\n            type: 'string'\n        },\n        textColor: {\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const RoutePathSourceSchema = {\n    type: 'string',\n    enum: ['NONE', 'TIMETABLE', 'ROUTED']\n} as const;\n\nexport const TransitRouteInfoSchema = {\n    type: 'object',\n    required: ['id', 'shortName', 'longName'],\n    properties: {\n        id: {\n            type: 'string'\n        },\n        shortName: {\n            type: 'string'\n        },\n        longName: {\n            type: 'string'\n        },\n        color: {\n            type: 'string'\n        },\n        textColor: {\n            type: 'string'\n        }\n    }\n} as const;\n\nexport const RouteInfoSchema = {\n    type: 'object',\n    required: ['mode', 'transitRoutes', 'numStops', 'routeIdx', 'pathSource', 'segments'],\n    properties: {\n        mode: {\n            '$ref': '#/components/schemas/Mode',\n            description: 'Transport mode for this route'\n        },\n        transitRoutes: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/TransitRouteInfo'\n            }\n        },\n        numStops: {\n            type: 'integer',\n            description: 'Number of stops along this route'\n        },\n        routeIdx: {\n            type: 'integer',\n            description: 'Internal route index for debugging purposes'\n        },\n        pathSource: {\n            '$ref': '#/components/schemas/RoutePathSource'\n        },\n        segments: {\n            type: 'array',\n            items: {\n                '$ref': '#/components/schemas/RouteSegment'\n            }\n        }\n    }\n} as const;"
  },
  {
    "path": "ui/api/openapi/services.gen.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport { createClient, createConfig, type Options } from '@hey-api/client-fetch';\nimport 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';\n\nexport const client = createClient(createConfig());\n\n/**\n * Computes optimal connections from one place to another.\n */\nexport const plan = <ThrowOnError extends boolean = false>(options: Options<PlanData, ThrowOnError>) => {\n    return (options?.client ?? client).get<PlanResponse, PlanError, ThrowOnError>({\n        ...options,\n        url: '/api/v5/plan'\n    });\n};\n\n/**\n * Street routing from one to many places or many to one.\n * The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the query.\n *\n */\nexport const oneToMany = <ThrowOnError extends boolean = false>(options: Options<OneToManyData, ThrowOnError>) => {\n    return (options?.client ?? client).get<OneToManyResponse, OneToManyError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/one-to-many'\n    });\n};\n\n/**\n * Street routing from one to many places or many to one.\n * The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the request body.\n *\n */\nexport const oneToManyPost = <ThrowOnError extends boolean = false>(options: Options<OneToManyPostData, ThrowOnError>) => {\n    return (options?.client ?? client).post<OneToManyPostResponse, OneToManyPostError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/one-to-many'\n    });\n};\n\n/**\n * One to many routing\n * Computes the minimal duration from one place to many or vice versa.\n * The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the query.\n *\n */\nexport const oneToManyIntermodal = <ThrowOnError extends boolean = false>(options: Options<OneToManyIntermodalData, ThrowOnError>) => {\n    return (options?.client ?? client).get<OneToManyIntermodalResponse2, OneToManyIntermodalError, ThrowOnError>({\n        ...options,\n        url: '/api/experimental/one-to-many-intermodal'\n    });\n};\n\n/**\n * One to many routing\n * Computes the minimal duration from one place to many or vice versa.\n * The order in the response array corresponds to the order of coordinates of the \\`many\\` parameter in the request body.\n *\n */\nexport const oneToManyIntermodalPost = <ThrowOnError extends boolean = false>(options: Options<OneToManyIntermodalPostData, ThrowOnError>) => {\n    return (options?.client ?? client).post<OneToManyIntermodalPostResponse, OneToManyIntermodalPostError, ThrowOnError>({\n        ...options,\n        url: '/api/experimental/one-to-many-intermodal'\n    });\n};\n\n/**\n * Computes all reachable locations from a given stop within a set duration.\n * Each result entry will contain the fastest travel duration and the number of connections used.\n *\n */\nexport const oneToAll = <ThrowOnError extends boolean = false>(options: Options<OneToAllData, ThrowOnError>) => {\n    return (options?.client ?? client).get<OneToAllResponse, OneToAllError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/one-to-all'\n    });\n};\n\n/**\n * Translate coordinates to the closest address(es)/places/stops.\n */\nexport const reverseGeocode = <ThrowOnError extends boolean = false>(options: Options<ReverseGeocodeData, ThrowOnError>) => {\n    return (options?.client ?? client).get<ReverseGeocodeResponse, ReverseGeocodeError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/reverse-geocode'\n    });\n};\n\n/**\n * Autocompletion & geocoding that resolves user input addresses including coordinates\n */\nexport const geocode = <ThrowOnError extends boolean = false>(options: Options<GeocodeData, ThrowOnError>) => {\n    return (options?.client ?? client).get<GeocodeResponse, GeocodeError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/geocode'\n    });\n};\n\n/**\n * Get a trip as itinerary\n */\nexport const trip = <ThrowOnError extends boolean = false>(options: Options<TripData, ThrowOnError>) => {\n    return (options?.client ?? client).get<TripResponse, TripError, ThrowOnError>({\n        ...options,\n        url: '/api/v5/trip'\n    });\n};\n\n/**\n * Get the next N departures or arrivals of a stop sorted by time\n */\nexport const stoptimes = <ThrowOnError extends boolean = false>(options?: Options<StoptimesData, ThrowOnError>) => {\n    return (options?.client ?? client).get<StoptimesResponse, StoptimesError, ThrowOnError>({\n        ...options,\n        url: '/api/v5/stoptimes'\n    });\n};\n\n/**\n * Given a area frame (box defined by top right and bottom left corner) and a time frame,\n * it returns all trips and their respective shapes that operate in this area + time frame.\n * Trips are filtered by zoom level. On low zoom levels, only long distance trains will be shown\n * while on high zoom levels, also metros, buses and trams will be returned.\n *\n */\nexport const trips = <ThrowOnError extends boolean = false>(options: Options<TripsData, ThrowOnError>) => {\n    return (options?.client ?? client).get<TripsResponse, TripsError, ThrowOnError>({\n        ...options,\n        url: '/api/v5/map/trips'\n    });\n};\n\n/**\n * initial location to view the map at after loading based on where public transport should be visible\n */\nexport const initial = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => {\n    return (options?.client ?? client).get<InitialResponse, InitialError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/map/initial'\n    });\n};\n\n/**\n * Get all stops for a map section\n */\nexport const stops = <ThrowOnError extends boolean = false>(options: Options<StopsData, ThrowOnError>) => {\n    return (options?.client ?? client).get<StopsResponse, StopsError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/map/stops'\n    });\n};\n\n/**\n * Get all available levels for a map section\n */\nexport const levels = <ThrowOnError extends boolean = false>(options: Options<LevelsData, ThrowOnError>) => {\n    return (options?.client ?? client).get<LevelsResponse, LevelsError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/map/levels'\n    });\n};\n\n/**\n * Given an area frame (box defined by the top-right and bottom-left corners),\n * it returns all routes and their respective shapes that operate within this area.\n * Routes are filtered by zoom level. On low zoom levels, only long distance trains will be shown\n * while on high zoom levels, also metros, buses and trams will be returned.\n *\n */\nexport const routes = <ThrowOnError extends boolean = false>(options: Options<RoutesData, ThrowOnError>) => {\n    return (options?.client ?? client).get<RoutesResponse, RoutesError, ThrowOnError>({\n        ...options,\n        url: '/api/experimental/map/routes'\n    });\n};\n\n/**\n * Returns the full data for a single route, including all stops and\n * polyline segments.\n *\n */\nexport const routeDetails = <ThrowOnError extends boolean = false>(options: Options<RouteDetailsData, ThrowOnError>) => {\n    return (options?.client ?? client).get<RouteDetailsResponse, RouteDetailsError, ThrowOnError>({\n        ...options,\n        url: '/api/experimental/map/route-details'\n    });\n};\n\n/**\n * Get a list of rental providers or all rental stations and vehicles for\n * a map section or provider\n *\n * Various options to filter the providers, stations and vehicles are\n * available. If none of these filters are provided, a list of all\n * available rental providers is returned without any station, vehicle or\n * zone data.\n *\n * At least one of the following filters must be provided to retrieve\n * station, vehicle and zone data:\n *\n * - A bounding box defined by the `min` and `max` parameters\n * - A circle defined by the `point` and `radius` parameters\n * - A list of provider groups via the `providerGroups` parameter\n * - A list of providers via the `providers` parameter\n *\n * Only data that matches all the provided filters is returned.\n *\n * Provide the `withProviders=false` parameter to retrieve only provider\n * groups if detailed feed information is not required.\n *\n */\nexport const rentals = <ThrowOnError extends boolean = false>(options?: Options<RentalsData, ThrowOnError>) => {\n    return (options?.client ?? client).get<RentalsResponse, RentalsError, ThrowOnError>({\n        ...options,\n        url: '/api/v1/rentals'\n    });\n};\n\n/**\n * Prints all transfers of a timetable location (track, bus stop, etc.)\n */\nexport const transfers = <ThrowOnError extends boolean = false>(options: Options<TransfersData, ThrowOnError>) => {\n    return (options?.client ?? client).get<TransfersResponse, TransfersError, ThrowOnError>({\n        ...options,\n        url: '/api/debug/transfers'\n    });\n};"
  },
  {
    "path": "ui/api/openapi/types.gen.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\n\n/**\n * Cause of this alert.\n */\nexport type AlertCause = 'UNKNOWN_CAUSE' | 'OTHER_CAUSE' | 'TECHNICAL_PROBLEM' | 'STRIKE' | 'DEMONSTRATION' | 'ACCIDENT' | 'HOLIDAY' | 'WEATHER' | 'MAINTENANCE' | 'CONSTRUCTION' | 'POLICE_ACTIVITY' | 'MEDICAL_EMERGENCY';\n\n/**\n * The effect of this problem on the affected entity.\n */\nexport type AlertEffect = 'NO_SERVICE' | 'REDUCED_SERVICE' | 'SIGNIFICANT_DELAYS' | 'DETOUR' | 'ADDITIONAL_SERVICE' | 'MODIFIED_SERVICE' | 'OTHER_EFFECT' | 'UNKNOWN_EFFECT' | 'STOP_MOVED' | 'NO_EFFECT' | 'ACCESSIBILITY_ISSUE';\n\n/**\n * The severity of the alert.\n */\nexport type AlertSeverityLevel = 'UNKNOWN_SEVERITY' | 'INFO' | 'WARNING' | 'SEVERE';\n\n/**\n * A time interval.\n * 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.\n *\n */\nexport type TimeRange = {\n    /**\n     * If missing, the interval starts at minus infinity.\n     * If a TimeRange is provided, either start or end must be provided - both fields cannot be empty.\n     *\n     */\n    start: string;\n    /**\n     * If missing, the interval ends at plus infinity.\n     * If a TimeRange is provided, either start or end must be provided - both fields cannot be empty.\n     *\n     */\n    end: string;\n};\n\n/**\n * An alert, indicating some sort of incident in the public transit network.\n */\nexport type Alert = {\n    /**\n     * Attribute or notice code (e.g. for HRDF or NeTEx)\n     */\n    code?: string;\n    /**\n     * Time when the alert should be shown to the user.\n     * If missing, the alert will be shown as long as it appears in the feed.\n     * If multiple ranges are given, the alert will be shown during all of them.\n     *\n     */\n    communicationPeriod?: Array<TimeRange>;\n    /**\n     * Time when the services are affected by the disruption mentioned in the alert.\n     */\n    impactPeriod?: Array<TimeRange>;\n    cause?: AlertCause;\n    /**\n     * Description of the cause of the alert that allows for agency-specific language;\n     * more specific than the Cause.\n     *\n     */\n    causeDetail?: string;\n    effect?: AlertEffect;\n    /**\n     * Description of the effect of the alert that allows for agency-specific language;\n     * more specific than the Effect.\n     *\n     */\n    effectDetail?: string;\n    /**\n     * The URL which provides additional information about the alert.\n     */\n    url?: string;\n    /**\n     * Header for the alert. This plain-text string will be highlighted, for example in boldface.\n     *\n     */\n    headerText: string;\n    /**\n     * Description for the alert.\n     * This plain-text string will be formatted as the body of the alert (or shown on an explicit \"expand\" request by the user).\n     * The information in the description should add to the information of the header.\n     *\n     */\n    descriptionText: string;\n    /**\n     * Text containing the alert's header to be used for text-to-speech implementations.\n     * This field is the text-to-speech version of header_text.\n     * It should contain the same information as headerText but formatted such that it can read as text-to-speech\n     * (for example, abbreviations removed, numbers spelled out, etc.)\n     *\n     */\n    ttsHeaderText?: string;\n    /**\n     * Text containing a description for the alert to be used for text-to-speech implementations.\n     * This field is the text-to-speech version of description_text.\n     * It should contain the same information as description_text but formatted such that it can be read as text-to-speech\n     * (for example, abbreviations removed, numbers spelled out, etc.)\n     *\n     */\n    ttsDescriptionText?: string;\n    /**\n     * Severity of the alert.\n     */\n    severityLevel?: AlertSeverityLevel;\n    /**\n     * String containing an URL linking to an image.\n     */\n    imageUrl?: string;\n    /**\n     * IANA media type as to specify the type of image to be displayed. The type must start with \"image/\"\n     *\n     */\n    imageMediaType?: string;\n    /**\n     * Text describing the appearance of the linked image in the image field\n     * (e.g., in case the image can't be displayed or the user can't see the image for accessibility reasons).\n     * See the HTML spec for alt image text.\n     *\n     */\n    imageAlternativeText?: string;\n};\n\n/**\n * Object containing duration if a path was found or none if no path was found\n */\nexport type Duration = {\n    /**\n     * duration in seconds if a path was found, otherwise missing\n     */\n    duration?: number;\n    /**\n     * distance in meters if a path was found and distance computation was requested, otherwise missing\n     */\n    distance?: number;\n};\n\n/**\n * Object containing a single element of a ParetoSet\n */\nexport type ParetoSetEntry = {\n    /**\n     * duration in seconds for the the best solution using `transfer` transfers\n     *\n     * Notice that the resolution is currently in minutes, because of implementation details\n     *\n     */\n    duration: number;\n    /**\n     * The minimal number of transfers required to arrive within `duration` seconds\n     *\n     * transfers=0: Direct transit connecion without any transfers\n     * transfers=1: Transit connection with 1 transfer\n     *\n     */\n    transfers: number;\n};\n\n/**\n * Pareto set of optimal transit solutions\n */\nexport type ParetoSet = Array<ParetoSetEntry>;\n\n/**\n * Object containing the optimal street and transit durations for One-to-Many routing\n */\nexport type OneToManyIntermodalResponse = {\n    /**\n     * Fastest durations for street routing\n     * The order of the items corresponds to the order of the `many` locations\n     * If no street routed connection is found, the corresponding `Duration` will be empty\n     *\n     */\n    street_durations?: Array<Duration>;\n    /**\n     * Pareto optimal solutions\n     * The order of the items corresponds to the order of the `many` locations\n     * If no connection using transits is found, the corresponding `ParetoSet` will be empty\n     *\n     */\n    transit_durations?: Array<ParetoSet>;\n};\n\n/**\n * Administrative area\n */\nexport type Area = {\n    /**\n     * Name of the area\n     */\n    name: string;\n    /**\n     * [OpenStreetMap `admin_level`](https://wiki.openstreetmap.org/wiki/Key:admin_level)\n     * of the area\n     *\n     */\n    adminLevel: number;\n    /**\n     * Whether this area was matched by the input text\n     */\n    matched: boolean;\n    /**\n     * Set for the first area after the `default` area that distinguishes areas\n     * if the match is ambiguous regarding (`default` area + place name / street [+ house number]).\n     *\n     */\n    unique?: boolean;\n    /**\n     * Whether this area should be displayed as default area (area with admin level closest 7)\n     */\n    default?: boolean;\n};\n\n/**\n * Matched token range (from index, length)\n */\nexport type Token = [\n    number,\n    number\n];\n\n/**\n * location type\n */\nexport type LocationType = 'ADDRESS' | 'PLACE' | 'STOP';\n\n/**\n * # Street modes\n *\n * - `WALK`\n * - `BIKE`\n * - `RENTAL` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs.\n * - `CAR`\n * - `CAR_PARKING` Experimental. Expect unannounced breaking changes (without version bumps) for all parameters and returned structs.\n * - `CAR_DROPOFF` Experimental. Expect unannounced breaking changes (without version bumps) for all perameters and returned structs.\n * - `ODM` on-demand taxis from the Prima+ÖV Project\n * - `RIDE_SHARING` ride sharing from the Prima+ÖV Project\n * - `FLEX` flexible transports\n *\n * # Transit modes\n *\n * - `TRANSIT`: translates to `TRAM,FERRY,AIRPLANE,BUS,COACH,RAIL,ODM,FUNICULAR,AERIAL_LIFT,OTHER`\n * - `TRAM`: trams\n * - `SUBWAY`: subway trains (Paris Metro, London Underground, but also NYC Subway, Hamburger Hochbahn, and other non-underground services)\n * - `FERRY`: ferries\n * - `AIRPLANE`: airline flights\n * - `BUS`: short distance buses (does not include `COACH`)\n * - `COACH`: long distance buses (does not include `BUS`)\n * - `RAIL`: translates to `HIGHSPEED_RAIL,LONG_DISTANCE,NIGHT_RAIL,REGIONAL_RAIL,SUBURBAN,SUBWAY`\n * - `HIGHSPEED_RAIL`: long distance high speed trains (e.g. TGV)\n * - `LONG_DISTANCE`: long distance inter city trains\n * - `NIGHT_RAIL`: long distance night trains\n * - `REGIONAL_FAST_RAIL`: deprecated, `REGIONAL_RAIL` will be used\n * - `REGIONAL_RAIL`: regional train\n * - `SUBURBAN`: suburban trains (e.g. S-Bahn, RER, Elizabeth Line, ...)\n * - `ODM`: demand responsive transport\n * - `FUNICULAR`: Funicular. Any rail system designed for steep inclines.\n * - `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.\n * - `AREAL_LIFT`: deprecated\n * - `METRO`: deprecated\n * - `CABLE_CAR`: deprecated\n *\n */\nexport 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';\n\n/**\n * GeoCoding match\n */\nexport type Match = {\n    type: LocationType;\n    /**\n     * Experimental. Type categories might be adjusted.\n     *\n     * For OSM stop locations: the amenity type based on\n     * https://wiki.openstreetmap.org/wiki/OpenStreetMap_Carto/Symbols\n     *\n     */\n    category?: string;\n    /**\n     * list of non-overlapping tokens that were matched\n     */\n    tokens: Array<Token>;\n    /**\n     * name of the location (transit stop / PoI / address)\n     */\n    name: string;\n    /**\n     * unique ID of the location\n     */\n    id: string;\n    /**\n     * latitude\n     */\n    lat: number;\n    /**\n     * longitude\n     */\n    lon: number;\n    /**\n     * level according to OpenStreetMap\n     * (at the moment only for public transport)\n     *\n     */\n    level?: number;\n    /**\n     * street name\n     */\n    street?: string;\n    /**\n     * house number\n     */\n    houseNumber?: string;\n    /**\n     * ISO3166-1 country code from OpenStreetMap\n     */\n    country?: string;\n    /**\n     * zip code\n     */\n    zip?: string;\n    /**\n     * timezone name (e.g. \"Europe/Berlin\")\n     */\n    tz?: string;\n    /**\n     * list of areas\n     */\n    areas: Array<Area>;\n    /**\n     * score according to the internal scoring system (the scoring algorithm might change in the future)\n     */\n    score: number;\n    /**\n     * available transport modes for stops\n     */\n    modes?: Array<Mode>;\n    /**\n     * importance of a stop, normalized from [0, 1]\n     */\n    importance?: number;\n};\n\n/**\n * Different elevation cost profiles for street routing.\n * Using a elevation cost profile will prefer routes with a smaller incline and smaller difference in elevation, even if the routed way is longer.\n *\n * - `NONE`: Ignore elevation data for routing. This is the default behavior\n * - `LOW`: Add a low penalty for inclines. This will favor longer paths, if the elevation increase and incline are smaller.\n * - `HIGH`: Add a high penalty for inclines. This will favor even longer paths, if the elevation increase and incline are smaller.\n *\n */\nexport type ElevationCosts = 'NONE' | 'LOW' | 'HIGH';\n\n/**\n * Different accessibility profiles for pedestrians.\n */\nexport type PedestrianProfile = 'FOOT' | 'WHEELCHAIR';\n\n/**\n * Average speed for pedestrian routing in meters per second\n */\nexport type PedestrianSpeed = number;\n\n/**\n * Average speed for bike routing in meters per second\n */\nexport type CyclingSpeed = number;\n\n/**\n * - `NORMAL` - latitude / longitude coordinate or address\n * - `BIKESHARE` - bike sharing station\n * - `TRANSIT` - transit stop\n *\n */\nexport type VertexType = 'NORMAL' | 'BIKESHARE' | 'TRANSIT';\n\n/**\n * - `NORMAL` - entry/exit is possible normally\n * - `NOT_ALLOWED` - entry/exit is not allowed\n *\n */\nexport type PickupDropoffType = 'NORMAL' | 'NOT_ALLOWED';\n\nexport type Place = {\n    /**\n     * name of the transit stop / PoI / address\n     */\n    name: string;\n    /**\n     * The ID of the stop. This is often something that users don't care about.\n     */\n    stopId?: string;\n    /**\n     * If it's not a root stop, this field contains the `stopId` of the parent stop.\n     */\n    parentId?: string;\n    /**\n     * The importance of the stop between 0-1.\n     */\n    importance?: number;\n    /**\n     * latitude\n     */\n    lat: number;\n    /**\n     * longitude\n     */\n    lon: number;\n    /**\n     * level according to OpenStreetMap\n     */\n    level: number;\n    /**\n     * timezone name (e.g. \"Europe/Berlin\")\n     */\n    tz?: string;\n    /**\n     * arrival time\n     */\n    arrival?: string;\n    /**\n     * departure time\n     */\n    departure?: string;\n    /**\n     * scheduled arrival time\n     */\n    scheduledArrival?: string;\n    /**\n     * scheduled departure time\n     */\n    scheduledDeparture?: string;\n    /**\n     * scheduled track from the static schedule timetable dataset\n     */\n    scheduledTrack?: string;\n    /**\n     * The current track/platform information, updated with real-time updates if available.\n     * Can be missing if neither real-time updates nor the schedule timetable contains track information.\n     *\n     */\n    track?: string;\n    /**\n     * description of the location that provides more detailed information\n     */\n    description?: string;\n    vertexType?: VertexType;\n    /**\n     * Type of pickup. It could be disallowed due to schedule, skipped stops or cancellations.\n     */\n    pickupType?: PickupDropoffType;\n    /**\n     * Type of dropoff. It could be disallowed due to schedule, skipped stops or cancellations.\n     */\n    dropoffType?: PickupDropoffType;\n    /**\n     * Whether this stop is cancelled due to the realtime situation.\n     */\n    cancelled?: boolean;\n    /**\n     * Alerts for this stop.\n     */\n    alerts?: Array<Alert>;\n    /**\n     * for `FLEX` transports, the flex location area or location group name\n     */\n    flex?: string;\n    /**\n     * for `FLEX` transports, the flex location area ID or location group ID\n     */\n    flexId?: string;\n    /**\n     * Time that on-demand service becomes available\n     */\n    flexStartPickupDropOffWindow?: string;\n    /**\n     * Time that on-demand service ends\n     */\n    flexEndPickupDropOffWindow?: string;\n    /**\n     * available transport modes for stops\n     */\n    modes?: Array<Mode>;\n};\n\n/**\n * Place reachable by One-to-All\n */\nexport type ReachablePlace = {\n    /**\n     * Place reached by One-to-All\n     */\n    place?: Place;\n    /**\n     * Total travel duration\n     */\n    duration?: number;\n    /**\n     * k is the smallest number, for which a journey with the shortest duration and at most k-1 transfers exist.\n     * You can think of k as the number of connections used.\n     *\n     * In more detail:\n     *\n     * k=0: No connection, e.g. for the one location\n     * k=1: Direct connection\n     * k=2: Connection with 1 transfer\n     *\n     */\n    k?: number;\n};\n\n/**\n * Object containing all reachable places by One-to-All search\n */\nexport type Reachable = {\n    /**\n     * One location used in One-to-All search\n     */\n    one?: Place;\n    /**\n     * List of locations reachable by One-to-All\n     */\n    all?: Array<ReachablePlace>;\n};\n\n/**\n * departure or arrival event at a stop\n */\nexport type StopTime = {\n    /**\n     * information about the stop place and time\n     */\n    place: Place;\n    /**\n     * Transport mode for this leg\n     */\n    mode: Mode;\n    /**\n     * Whether there is real-time data about this leg\n     */\n    realTime: boolean;\n    /**\n     * The headsign of the bus or train being used.\n     * For non-transit legs, null\n     *\n     */\n    headsign: string;\n    /**\n     * first stop of this trip\n     */\n    tripFrom: Place;\n    /**\n     * final stop of this trip\n     */\n    tripTo: Place;\n    agencyId: string;\n    agencyName: string;\n    agencyUrl: string;\n    routeId: string;\n    routeUrl?: string;\n    directionId: string;\n    routeColor?: string;\n    routeTextColor?: string;\n    tripId: string;\n    routeType?: number;\n    routeShortName: string;\n    routeLongName: string;\n    tripShortName: string;\n    displayName: string;\n    /**\n     * Experimental. Expect unannounced breaking changes (without version bumps).\n     *\n     * Stops on the trips before this stop. Returned only if `fetchStop` and `arriveBy` are `true`.\n     *\n     */\n    previousStops?: Array<Place>;\n    /**\n     * Experimental. Expect unannounced breaking changes (without version bumps).\n     *\n     * Stops on the trips after this stop. Returned only if `fetchStop` is `true` and `arriveBy` is `false`.\n     *\n     */\n    nextStops?: Array<Place>;\n    /**\n     * Type of pickup (for departures) or dropoff (for arrivals), may be disallowed either due to schedule, skipped stops or cancellations\n     */\n    pickupDropoffType: PickupDropoffType;\n    /**\n     * Whether the departure/arrival is cancelled due to the realtime situation (either because the stop is skipped or because the entire trip is cancelled).\n     */\n    cancelled: boolean;\n    /**\n     * Whether the entire trip is cancelled due to the realtime situation.\n     */\n    tripCancelled: boolean;\n    /**\n     * Filename and line number where this trip is from\n     */\n    source: string;\n};\n\n/**\n * trip id and name\n */\nexport type TripInfo = {\n    /**\n     * trip ID (dataset trip id prefixed with the dataset tag)\n     */\n    tripId: string;\n    /**\n     * trip display name (api version < 4)\n     */\n    routeShortName?: string;\n    /**\n     * trip display name (api version >= 4)\n     */\n    displayName?: string;\n};\n\n/**\n * trip segment between two stops to show a trip on a map\n */\nexport type TripSegment = {\n    trips: Array<TripInfo>;\n    routeColor?: string;\n    /**\n     * Transport mode for this leg\n     */\n    mode: Mode;\n    /**\n     * distance in meters\n     */\n    distance: number;\n    from: Place;\n    to: Place;\n    /**\n     * departure time\n     */\n    departure: string;\n    /**\n     * arrival time\n     */\n    arrival: string;\n    /**\n     * scheduled departure time\n     */\n    scheduledDeparture: string;\n    /**\n     * scheduled arrival time\n     */\n    scheduledArrival: string;\n    /**\n     * Whether there is real-time data about this leg\n     */\n    realTime: boolean;\n    /**\n     * Google polyline encoded coordinate sequence (with precision 5) where the trip travels on this segment.\n     */\n    polyline: string;\n};\n\nexport type Direction = 'DEPART' | 'HARD_LEFT' | 'LEFT' | 'SLIGHTLY_LEFT' | 'CONTINUE' | 'SLIGHTLY_RIGHT' | 'RIGHT' | 'HARD_RIGHT' | 'CIRCLE_CLOCKWISE' | 'CIRCLE_COUNTERCLOCKWISE' | 'STAIRS' | 'ELEVATOR' | 'UTURN_LEFT' | 'UTURN_RIGHT';\n\nexport type EncodedPolyline = {\n    /**\n     * The encoded points of the polyline using the Google polyline encoding.\n     */\n    points: string;\n    /**\n     * The precision of the returned polyline (7 for /v1, 6 for /v2)\n     * Be aware that with precision 7, coordinates with |longitude| > 107.37 are undefined/will overflow.\n     *\n     */\n    precision: number;\n    /**\n     * The number of points in the string\n     */\n    length: number;\n};\n\nexport type StepInstruction = {\n    relativeDirection: Direction;\n    /**\n     * The distance in meters that this step takes.\n     */\n    distance: number;\n    /**\n     * level where this segment starts, based on OpenStreetMap data\n     */\n    fromLevel: number;\n    /**\n     * level where this segment starts, based on OpenStreetMap data\n     */\n    toLevel: number;\n    /**\n     * OpenStreetMap way index\n     */\n    osmWay?: number;\n    polyline: EncodedPolyline;\n    /**\n     * The name of the street.\n     */\n    streetName: string;\n    /**\n     * Not implemented!\n     * When exiting a highway or traffic circle, the exit name/number.\n     *\n     */\n    exit: string;\n    /**\n     * Not implemented!\n     * Indicates whether or not a street changes direction at an intersection.\n     *\n     */\n    stayOn: boolean;\n    /**\n     * Not implemented!\n     * This step is on an open area, such as a plaza or train platform,\n     * and thus the directions should say something like \"cross\"\n     *\n     */\n    area: boolean;\n    /**\n     * Indicates that a fee must be paid by general traffic to use a road, road bridge or road tunnel.\n     */\n    toll?: boolean;\n    /**\n     * Experimental. Indicates whether access to this part of the route is restricted.\n     * See: https://wiki.openstreetmap.org/wiki/Conditional_restrictions\n     *\n     */\n    accessRestriction?: string;\n    /**\n     * incline in meters across this path segment\n     */\n    elevationUp?: number;\n    /**\n     * decline in meters across this path segment\n     */\n    elevationDown?: number;\n};\n\nexport type RentalFormFactor = 'BICYCLE' | 'CARGO_BICYCLE' | 'CAR' | 'MOPED' | 'SCOOTER_STANDING' | 'SCOOTER_SEATED' | 'OTHER';\n\nexport type RentalPropulsionType = 'HUMAN' | 'ELECTRIC_ASSIST' | 'ELECTRIC' | 'COMBUSTION' | 'COMBUSTION_DIESEL' | 'HYBRID' | 'PLUG_IN_HYBRID' | 'HYDROGEN_FUEL_CELL';\n\nexport type RentalReturnConstraint = 'NONE' | 'ANY_STATION' | 'ROUNDTRIP_STATION';\n\n/**\n * Vehicle rental\n */\nexport type Rental = {\n    /**\n     * Rental provider ID\n     */\n    providerId: string;\n    /**\n     * Rental provider group ID\n     */\n    providerGroupId: string;\n    /**\n     * Vehicle share system ID\n     */\n    systemId: string;\n    /**\n     * Vehicle share system name\n     */\n    systemName?: string;\n    /**\n     * URL of the vehicle share system\n     */\n    url?: string;\n    /**\n     * Color associated with this provider, in hexadecimal RGB format\n     * (e.g. \"#FF0000\" for red). Can be empty.\n     *\n     */\n    color?: string;\n    /**\n     * Name of the station\n     */\n    stationName?: string;\n    /**\n     * Name of the station where the vehicle is picked up (empty for free floating vehicles)\n     */\n    fromStationName?: string;\n    /**\n     * Name of the station where the vehicle is returned (empty for free floating vehicles)\n     */\n    toStationName?: string;\n    /**\n     * Rental URI for Android (deep link to the specific station or vehicle)\n     */\n    rentalUriAndroid?: string;\n    /**\n     * Rental URI for iOS (deep link to the specific station or vehicle)\n     */\n    rentalUriIOS?: string;\n    /**\n     * Rental URI for web (deep link to the specific station or vehicle)\n     */\n    rentalUriWeb?: string;\n    formFactor?: RentalFormFactor;\n    propulsionType?: RentalPropulsionType;\n    returnConstraint?: RentalReturnConstraint;\n};\n\n/**\n * A multi-polygon contains a number of polygons, each containing a number\n * of rings, which are encoded as polylines (with precision 6).\n *\n * For each polygon, the first ring is the outer ring, all subsequent rings\n * are inner rings (holes).\n *\n */\nexport type MultiPolygon = Array<Array<EncodedPolyline>>;\n\nexport type RentalZoneRestrictions = {\n    /**\n     * List of vehicle types (as indices into the provider's vehicle types\n     * array) to which these restrictions apply.\n     * If empty, the restrictions apply to all vehicle types of the provider.\n     *\n     */\n    vehicleTypeIdxs: Array<(number)>;\n    /**\n     * whether the ride is allowed to start in this zone\n     */\n    rideStartAllowed: boolean;\n    /**\n     * whether the ride is allowed to end in this zone\n     */\n    rideEndAllowed: boolean;\n    /**\n     * whether the ride is allowed to pass through this zone\n     */\n    rideThroughAllowed: boolean;\n    /**\n     * whether vehicles can only be parked at stations in this zone\n     */\n    stationParking?: boolean;\n};\n\nexport type RentalVehicleType = {\n    /**\n     * Unique identifier of the vehicle type (unique within the provider)\n     */\n    id: string;\n    /**\n     * Public name of the vehicle type (can be empty)\n     */\n    name?: string;\n    formFactor: RentalFormFactor;\n    propulsionType: RentalPropulsionType;\n    returnConstraint: RentalReturnConstraint;\n    /**\n     * Whether the return constraint was guessed instead of being specified by the rental provider\n     */\n    returnConstraintGuessed: boolean;\n};\n\nexport type RentalProvider = {\n    /**\n     * Unique identifier of the rental provider\n     */\n    id: string;\n    /**\n     * Name of the provider to be displayed to customers\n     */\n    name: string;\n    /**\n     * Id of the rental provider group this provider belongs to\n     */\n    groupId: string;\n    /**\n     * Name of the system operator\n     */\n    operator?: string;\n    /**\n     * URL of the vehicle share system\n     */\n    url?: string;\n    /**\n     * URL where a customer can purchase a membership\n     */\n    purchaseUrl?: string;\n    /**\n     * Color associated with this provider, in hexadecimal RGB format\n     * (e.g. \"#FF0000\" for red). Can be empty.\n     *\n     */\n    color?: string;\n    /**\n     * Bounding box of the area covered by this rental provider,\n     * [west, south, east, north] as [lon, lat, lon, lat]\n     *\n     */\n    bbox: [\n        number,\n        number,\n        number,\n        number\n    ];\n    vehicleTypes: Array<RentalVehicleType>;\n    /**\n     * List of form factors offered by this provider\n     */\n    formFactors: Array<RentalFormFactor>;\n    defaultRestrictions: RentalZoneRestrictions;\n    globalGeofencingRules: Array<RentalZoneRestrictions>;\n};\n\nexport type RentalProviderGroup = {\n    /**\n     * Unique identifier of the rental provider group\n     */\n    id: string;\n    /**\n     * Name of the provider group to be displayed to customers\n     */\n    name: string;\n    /**\n     * Color associated with this provider group, in hexadecimal RGB format\n     * (e.g. \"#FF0000\" for red). Can be empty.\n     *\n     */\n    color?: string;\n    /**\n     * List of rental provider IDs that belong to this group\n     */\n    providers: Array<(string)>;\n    /**\n     * List of form factors offered by this provider group\n     */\n    formFactors: Array<RentalFormFactor>;\n};\n\nexport type RentalStation = {\n    /**\n     * Unique identifier of the rental station\n     */\n    id: string;\n    /**\n     * Unique identifier of the rental provider\n     */\n    providerId: string;\n    /**\n     * Unique identifier of the rental provider group\n     */\n    providerGroupId: string;\n    /**\n     * Public name of the station\n     */\n    name: string;\n    /**\n     * latitude\n     */\n    lat: number;\n    /**\n     * longitude\n     */\n    lon: number;\n    /**\n     * Address where the station is located\n     */\n    address?: string;\n    /**\n     * Cross street or landmark where the station is located\n     */\n    crossStreet?: string;\n    /**\n     * Rental URI for Android (deep link to the specific station)\n     */\n    rentalUriAndroid?: string;\n    /**\n     * Rental URI for iOS (deep link to the specific station)\n     */\n    rentalUriIOS?: string;\n    /**\n     * Rental URI for web (deep link to the specific station)\n     */\n    rentalUriWeb?: string;\n    /**\n     * true if vehicles can be rented from this station, false if it is temporarily out of service\n     */\n    isRenting: boolean;\n    /**\n     * true if vehicles can be returned to this station, false if it is temporarily out of service\n     */\n    isReturning: boolean;\n    /**\n     * Number of vehicles available for rental at this station\n     */\n    numVehiclesAvailable: number;\n    /**\n     * List of form factors available for rental and/or return at this station\n     */\n    formFactors: Array<RentalFormFactor>;\n    /**\n     * List of vehicle types currently available at this station (vehicle type ID -> count)\n     */\n    vehicleTypesAvailable: {\n        [key: string]: (number);\n    };\n    /**\n     * List of vehicle docks currently available at this station (vehicle type ID -> count)\n     */\n    vehicleDocksAvailable: {\n        [key: string]: (number);\n    };\n    stationArea?: MultiPolygon;\n    /**\n     * Bounding box of the area covered by this station,\n     * [west, south, east, north] as [lon, lat, lon, lat]\n     *\n     */\n    bbox: [\n        number,\n        number,\n        number,\n        number\n    ];\n};\n\nexport type RentalVehicle = {\n    /**\n     * Unique identifier of the rental vehicle\n     */\n    id: string;\n    /**\n     * Unique identifier of the rental provider\n     */\n    providerId: string;\n    /**\n     * Unique identifier of the rental provider group\n     */\n    providerGroupId: string;\n    /**\n     * Vehicle type ID (unique within the provider)\n     */\n    typeId: string;\n    /**\n     * latitude\n     */\n    lat: number;\n    /**\n     * longitude\n     */\n    lon: number;\n    formFactor: RentalFormFactor;\n    propulsionType: RentalPropulsionType;\n    returnConstraint: RentalReturnConstraint;\n    /**\n     * Station ID if the vehicle is currently at a station\n     */\n    stationId?: string;\n    /**\n     * Station ID where the vehicle must be returned, if applicable\n     */\n    homeStationId?: string;\n    /**\n     * true if the vehicle is currently reserved by a customer, false otherwise\n     */\n    isReserved: boolean;\n    /**\n     * true if the vehicle is out of service, false otherwise\n     */\n    isDisabled: boolean;\n    /**\n     * Rental URI for Android (deep link to the specific vehicle)\n     */\n    rentalUriAndroid?: string;\n    /**\n     * Rental URI for iOS (deep link to the specific vehicle)\n     */\n    rentalUriIOS?: string;\n    /**\n     * Rental URI for web (deep link to the specific vehicle)\n     */\n    rentalUriWeb?: string;\n};\n\nexport type RentalZone = {\n    /**\n     * Unique identifier of the rental provider\n     */\n    providerId: string;\n    /**\n     * Unique identifier of the rental provider group\n     */\n    providerGroupId: string;\n    /**\n     * Public name of the geofencing zone\n     */\n    name?: string;\n    /**\n     * Zone precedence / z-index (higher number = higher precedence)\n     */\n    z: number;\n    /**\n     * Bounding box of the area covered by this zone,\n     * [west, south, east, north] as [lon, lat, lon, lat]\n     *\n     */\n    bbox: [\n        number,\n        number,\n        number,\n        number\n    ];\n    area: MultiPolygon;\n    rules: Array<RentalZoneRestrictions>;\n};\n\n/**\n * not available for GTFS datasets by default\n * For NeTEx it contains information about the vehicle category, e.g. IC/InterCity\n *\n */\nexport type Category = {\n    id: string;\n    name: string;\n    shortName: string;\n};\n\nexport type Leg = {\n    /**\n     * Transport mode for this leg\n     */\n    mode: Mode;\n    from: Place;\n    to: Place;\n    /**\n     * Leg duration in seconds\n     *\n     * If leg is footpath:\n     * The footpath duration is derived from the default footpath\n     * duration using the query parameters `transferTimeFactor` and\n     * `additionalTransferTime` as follows:\n     * `leg.duration = defaultDuration * transferTimeFactor + additionalTransferTime.`\n     * In case the defaultDuration is needed, it can be calculated by\n     * `defaultDuration = (leg.duration - additionalTransferTime) / transferTimeFactor`.\n     * Note that the default values are `transferTimeFactor = 1` and\n     * `additionalTransferTime = 0` in case they are not explicitly\n     * provided in the query.\n     *\n     */\n    duration: number;\n    /**\n     * leg departure time\n     */\n    startTime: string;\n    /**\n     * leg arrival time\n     */\n    endTime: string;\n    /**\n     * scheduled leg departure time\n     */\n    scheduledStartTime: string;\n    /**\n     * scheduled leg arrival time\n     */\n    scheduledEndTime: string;\n    /**\n     * Whether there is real-time data about this leg\n     */\n    realTime: boolean;\n    /**\n     * Whether this leg was originally scheduled to run or is an additional service.\n     * Scheduled times will equal realtime times in this case.\n     *\n     */\n    scheduled: boolean;\n    /**\n     * For non-transit legs the distance traveled while traversing this leg in meters.\n     */\n    distance?: number;\n    /**\n     * For transit legs, if the rider should stay on the vehicle as it changes route names.\n     */\n    interlineWithPreviousLeg?: boolean;\n    /**\n     * For transit legs, the headsign of the bus or train being used.\n     * For non-transit legs, null\n     *\n     */\n    headsign?: string;\n    /**\n     * first stop of this trip\n     */\n    tripFrom?: Place;\n    /**\n     * final stop of this trip (can differ from headsign)\n     */\n    tripTo?: Place;\n    category?: Category;\n    routeId?: string;\n    routeUrl?: string;\n    directionId?: string;\n    routeColor?: string;\n    routeTextColor?: string;\n    routeType?: number;\n    agencyName?: string;\n    agencyUrl?: string;\n    agencyId?: string;\n    tripId?: string;\n    routeShortName?: string;\n    routeLongName?: string;\n    tripShortName?: string;\n    displayName?: string;\n    /**\n     * Whether this trip is cancelled\n     */\n    cancelled?: boolean;\n    /**\n     * Filename and line number where this trip is from\n     */\n    source?: string;\n    /**\n     * For transit legs, intermediate stops between the Place where the leg originates\n     * and the Place where the leg ends. For non-transit legs, null.\n     *\n     */\n    intermediateStops?: Array<Place>;\n    /**\n     * Encoded geometry of the leg.\n     * If detailed leg output is disabled, this is returned as an empty\n     * polyline.\n     *\n     */\n    legGeometry: EncodedPolyline;\n    /**\n     * A series of turn by turn instructions\n     * used for walking, biking and driving.\n     * This field is omitted if the request disables detailed leg output.\n     *\n     */\n    steps?: Array<StepInstruction>;\n    rental?: Rental;\n    /**\n     * Index into `Itinerary.fareTransfers` array\n     * to identify which fare transfer this leg belongs to\n     *\n     */\n    fareTransferIndex?: number;\n    /**\n     * Index into the `Itinerary.fareTransfers[fareTransferIndex].effectiveFareLegProducts` array\n     * to identify which effective fare leg this itinerary leg belongs to\n     *\n     */\n    effectiveFareLegIndex?: number;\n    /**\n     * Alerts for this stop.\n     */\n    alerts?: Array<Alert>;\n    /**\n     * If set, this attribute indicates that this trip has been expanded\n     * beyond the feed end date (enabled by config flag `timetable.dataset.extend_calendar`)\n     * by looping active weekdays, e.g. from calendar.txt in GTFS.\n     *\n     */\n    loopedCalendarSince?: string;\n    /**\n     * Whether bikes can be carried on this leg.\n     *\n     */\n    bikesAllowed?: boolean;\n};\n\nexport type RiderCategory = {\n    /**\n     * Rider category name as displayed to the rider.\n     */\n    riderCategoryName: string;\n    /**\n     * Specifies if this category should be considered the default (i.e. the main category displayed to riders).\n     */\n    isDefaultFareCategory: boolean;\n    /**\n     * URL to a web page providing detailed information about the rider category and/or its eligibility criteria.\n     */\n    eligibilityUrl?: string;\n};\n\n/**\n * - `NONE`: No fare media involved (e.g., cash payment)\n * - `PAPER_TICKET`: Physical paper ticket\n * - `TRANSIT_CARD`: Physical transit card with stored value\n * - `CONTACTLESS_EMV`: cEMV (contactless payment)\n * - `MOBILE_APP`: Mobile app with virtual transit cards/passes\n *\n */\nexport type FareMediaType = 'NONE' | 'PAPER_TICKET' | 'TRANSIT_CARD' | 'CONTACTLESS_EMV' | 'MOBILE_APP';\n\nexport type FareMedia = {\n    /**\n     * Name of the fare media. Required for transit cards and mobile apps.\n     */\n    fareMediaName?: string;\n    /**\n     * The type of fare media.\n     */\n    fareMediaType: FareMediaType;\n};\n\nexport type FareProduct = {\n    /**\n     * The name of the fare product as displayed to riders.\n     */\n    name: string;\n    /**\n     * The cost of the fare product. May be negative to represent transfer discounts. May be zero to represent a fare product that is free.\n     */\n    amount: number;\n    /**\n     * ISO 4217 currency code. The currency of the cost of the fare product.\n     */\n    currency: string;\n    riderCategory?: RiderCategory;\n    media?: FareMedia;\n};\n\nexport type FareTransferRule = 'A_AB' | 'A_AB_B' | 'AB';\n\n/**\n * The concept is derived from: https://gtfs.org/documentation/schedule/reference/#fare_transfer_rulestxt\n *\n * Terminology:\n * - **Leg**: An itinerary leg as described by the `Leg` type of this API description.\n * - **Effective Fare Leg**: Itinerary legs can be joined together to form one *effective fare leg*.\n * - **Fare Transfer**: A fare transfer groups two or more effective fare legs.\n * - **A** is the first *effective fare leg* of potentially multiple consecutive legs contained in a fare transfer\n * - **B** is any *effective fare leg* following the first *effective fare leg* in this transfer\n * - **AB** are all changes between *effective fare legs* contained in this transfer\n *\n * The fare transfer rule is used to derive the final set of products of the itinerary legs contained in this transfer:\n * - 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.\n * - 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)\n * - 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.\n *\n * An itinerary `Leg` references the index of the fare transfer and the index of the effective fare leg in this transfer it belongs to.\n *\n */\nexport type FareTransfer = {\n    rule?: FareTransferRule;\n    transferProducts?: Array<FareProduct>;\n    /**\n     * Lists all valid fare products for the effective fare legs.\n     * This is an `array<array<FareProduct>>` where the inner array\n     * lists all possible fare products that would cover this effective fare leg.\n     * Each \"effective fare leg\" can have multiple options for adult/child/weekly/monthly/day/one-way tickets etc.\n     * 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`)\n     * and the inner array as OR (you can choose which ticket to buy)\n     *\n     */\n    effectiveFareLegProducts: Array<Array<Array<FareProduct>>>;\n};\n\nexport type Itinerary = {\n    /**\n     * journey duration in seconds\n     */\n    duration: number;\n    /**\n     * journey departure time\n     */\n    startTime: string;\n    /**\n     * journey arrival time\n     */\n    endTime: string;\n    /**\n     * The number of transfers this trip has.\n     */\n    transfers: number;\n    /**\n     * Journey legs\n     */\n    legs: Array<Leg>;\n    /**\n     * Fare information\n     */\n    fareTransfers?: Array<FareTransfer>;\n};\n\n/**\n * transfer from one location to another\n */\nexport type Transfer = {\n    to: Place;\n    /**\n     * optional; missing if the GTFS did not contain a transfer\n     * transfer duration in minutes according to GTFS (+heuristics)\n     *\n     */\n    default?: number;\n    /**\n     * optional; missing if no path was found (timetable / osr)\n     * transfer duration in minutes for the foot profile\n     *\n     */\n    foot?: number;\n    /**\n     * optional; missing if no path was found with foot routing\n     * transfer duration in minutes for the foot profile\n     *\n     */\n    footRouted?: number;\n    /**\n     * optional; missing if no path was found with the wheelchair profile\n     * transfer duration in minutes for the wheelchair profile\n     *\n     */\n    wheelchair?: number;\n    /**\n     * optional; missing if no path was found with the wheelchair profile\n     * transfer duration in minutes for the wheelchair profile\n     *\n     */\n    wheelchairRouted?: number;\n    /**\n     * optional; missing if no path was found with the wheelchair profile\n     * true if the wheelchair path uses an elevator\n     *\n     */\n    wheelchairUsesElevator?: boolean;\n    /**\n     * optional; missing if no path was found with car routing\n     * transfer duration in minutes for the car profile\n     *\n     */\n    car?: number;\n};\n\nexport type OneToManyParams = {\n    /**\n     * geo location as latitude;longitude\n     */\n    one: string;\n    /**\n     * geo locations as latitude;longitude,latitude;longitude,...\n     *\n     * The number of accepted locations is limited by server config variable `onetomany_max_many`.\n     *\n     */\n    many: Array<(string)>;\n    /**\n     * routing profile to use (currently supported: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`)\n     *\n     */\n    mode: Mode;\n    /**\n     * maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`.\n     */\n    max: number;\n    /**\n     * maximum matching distance in meters to match geo coordinates to the street network\n     */\n    maxMatchingDistance: number;\n    /**\n     * Optional. Default is `NONE`.\n     *\n     * Set an elevation cost profile, to penalize routes with incline.\n     * - `NONE`: No additional costs for elevations. This is the default behavior\n     * - `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.\n     * - `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.\n     *\n     * As using an elevation costs profile will increase the travel duration,\n     * routing through steep terrain may exceed the maximal allowed duration,\n     * causing a location to appear unreachable.\n     * Increasing the maximum travel time for these segments may resolve this issue.\n     *\n     * Elevation cost profiles are currently used by following street modes:\n     * - `BIKE`\n     *\n     */\n    elevationCosts?: ElevationCosts;\n    /**\n     * true = many to one\n     * false = one to many\n     *\n     */\n    arriveBy: boolean;\n    /**\n     * If true, the response includes the distance in meters\n     * for each path. This requires path reconstruction and\n     * may be slower than duration-only queries.\n     *\n     */\n    withDistance?: boolean;\n};\n\nexport type OneToManyIntermodalParams = {\n    /**\n     * \\`latitude,longitude[,level]\\` tuple with\n     * - latitude and longitude in degrees\n     * - (optional) level: the OSM level (default: 0)\n     *\n     * OR\n     *\n     * stop id\n     *\n     */\n    one: string;\n    /**\n     * array of:\n     *\n     * \\`latitude,longitude[,level]\\` tuple with\n     * - latitude and longitude in degrees\n     * - (optional) level: the OSM level (default: 0)\n     *\n     * OR\n     *\n     * stop id\n     *\n     * The number of accepted locations is limited by server config variable `onetomany_max_many`.\n     *\n     */\n    many: Array<(string)>;\n    /**\n     * Optional. Defaults to the current time.\n     *\n     * Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n     *\n     */\n    time?: string;\n    /**\n     * The maximum travel time in minutes.\n     * If not provided, the routing uses the value\n     * hardcoded in the server which is usually quite high.\n     *\n     * *Warning*: Use with care. Setting this too low can lead to\n     * optimal (e.g. the least transfers) journeys not being found.\n     * If this value is too low to reach the destination at all,\n     * it can lead to slow routing performance.\n     *\n     */\n    maxTravelTime?: number;\n    /**\n     * maximum matching distance in meters to match geo coordinates to the street network\n     */\n    maxMatchingDistance?: number;\n    /**\n     * Optional. Defaults to false, i.e. one to many search\n     *\n     * true = many to one\n     * false = one to many\n     *\n     */\n    arriveBy?: boolean;\n    /**\n     * The maximum number of allowed transfers (i.e. interchanges between transit legs,\n     * pre- and postTransit do not count as transfers).\n     * `maxTransfers=0` searches for direct transit connections without any transfers.\n     * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n     * send an empty `transitModes` parameter instead.\n     *\n     * If not provided, the routing uses the server-side default value\n     * which is hardcoded and very high to cover all use cases.\n     *\n     * *Warning*: Use with care. Setting this too low can lead to\n     * optimal (e.g. the fastest) journeys not being found.\n     * If this value is too low to reach the destination at all,\n     * it can lead to slow routing performance.\n     *\n     */\n    maxTransfers?: number;\n    /**\n     * Optional. Default is 0 minutes.\n     *\n     * Minimum transfer time for each transfer in minutes.\n     *\n     */\n    minTransferTime?: number;\n    /**\n     * Optional. Default is 0 minutes.\n     *\n     * Additional transfer time reserved for each transfer in minutes.\n     *\n     */\n    additionalTransferTime?: number;\n    /**\n     * Optional. Default is 1.0\n     *\n     * Factor to multiply minimum required transfer times with.\n     * Values smaller than 1.0 are not supported.\n     *\n     */\n    transferTimeFactor?: number;\n    /**\n     * Optional. Default is `false`.\n     *\n     * Whether to use transfers routed on OpenStreetMap data.\n     *\n     */\n    useRoutedTransfers?: boolean;\n    /**\n     * Optional. Default is `FOOT`.\n     *\n     * Accessibility profile to use for pedestrian routing in transfers\n     * between transit connections and the first and last mile respectively.\n     *\n     */\n    pedestrianProfile?: PedestrianProfile;\n    /**\n     * Optional\n     *\n     * Average speed for pedestrian routing.\n     *\n     */\n    pedestrianSpeed?: PedestrianSpeed;\n    /**\n     * Optional\n     *\n     * Average speed for bike routing.\n     *\n     */\n    cyclingSpeed?: CyclingSpeed;\n    /**\n     * Optional. Default is `NONE`.\n     *\n     * Set an elevation cost profile, to penalize routes with incline.\n     * - `NONE`: No additional costs for elevations. This is the default behavior\n     * - `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.\n     * - `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.\n     *\n     * As using an elevation costs profile will increase the travel duration,\n     * routing through steep terrain may exceed the maximal allowed duration,\n     * causing a location to appear unreachable.\n     * Increasing the maximum travel time for these segments may resolve this issue.\n     *\n     * The profile is used for routing on both the first and last mile.\n     *\n     * Elevation cost profiles are currently used by following street modes:\n     * - `BIKE`\n     *\n     */\n    elevationCosts?: ElevationCosts;\n    /**\n     * Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n     * Allowed modes for the transit part. If empty, no transit connections will be computed.\n     * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n     *\n     */\n    transitModes?: Array<Mode>;\n    /**\n     * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n     *\n     * 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`.\n     *\n     */\n    preTransitModes?: Array<Mode>;\n    /**\n     * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n     *\n     * 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`.\n     *\n     */\n    postTransitModes?: Array<Mode>;\n    /**\n     * Default is `WALK` which will compute walking routes as direct connections.\n     *\n     * Mode used for direction connections from start to destination without using transit.\n     *\n     * Currently supported non-transit modes: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`\n     *\n     */\n    directMode?: Mode;\n    /**\n     * Optional. Default is 15min which is `900`.\n     * Maximum time in seconds for the first street leg.\n     * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n     *\n     */\n    maxPreTransitTime?: number;\n    /**\n     * Optional. Default is 15min which is `900`.\n     * Maximum time in seconds for the last street leg.\n     * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n     *\n     */\n    maxPostTransitTime?: number;\n    /**\n     * Optional. Default is 30min which is `1800`.\n     * Maximum time in seconds for direct connections.\n     *\n     * If a value smaller than either `maxPreTransitTime` or\n     * `maxPostTransitTime` is used, their maximum is set instead.\n     * Is limited by server config variable `street_routing_max_direct_seconds`.\n     *\n     */\n    maxDirectTime?: number;\n    /**\n     * If true, the response includes the distance in meters\n     * for each path. This requires path reconstruction and\n     * may be slower than duration-only queries.\n     *\n     * `withDistance` is currently limited to street routing.\n     *\n     */\n    withDistance?: boolean;\n    /**\n     * Optional. Default is `false`.\n     *\n     * If set to `true`, all used transit trips are required to allow bike carriage.\n     *\n     */\n    requireBikeTransport?: boolean;\n    /**\n     * Optional. Default is `false`.\n     *\n     * If set to `true`, all used transit trips are required to allow car carriage.\n     *\n     */\n    requireCarTransport?: boolean;\n};\n\nexport type ServerConfig = {\n    /**\n     * the version of this MOTIS server\n     */\n    motisVersion: string;\n    /**\n     * true if elevation is loaded\n     */\n    hasElevation: boolean;\n    /**\n     * true if routed transfers available\n     */\n    hasRoutedTransfers: boolean;\n    /**\n     * true if street routing is available\n     */\n    hasStreetRouting: boolean;\n    /**\n     * limit for the number of `many` locations for one-to-many requests\n     *\n     */\n    maxOneToManySize: number;\n    /**\n     * limit for maxTravelTime API param in minutes\n     */\n    maxOneToAllTravelTimeLimit: number;\n    /**\n     * limit for maxPrePostTransitTime API param in seconds\n     */\n    maxPrePostTransitTimeLimit: number;\n    /**\n     * limit for maxDirectTime API param in seconds\n     */\n    maxDirectTimeLimit: number;\n    /**\n     * true if experimental route shapes debug download API is enabled\n     */\n    shapesDebugEnabled: boolean;\n};\n\nexport type Error = {\n    /**\n     * error message\n     */\n    error: string;\n};\n\n/**\n * Route segment between two stops to show a route on a map\n */\nexport type RouteSegment = {\n    /**\n     * Index into the top-level route stops array\n     */\n    from: number;\n    /**\n     * Index into the top-level route stops array\n     */\n    to: number;\n    /**\n     * Index into the top-level route polylines array\n     */\n    polyline: number;\n};\n\n/**\n * Shared polyline used by one or more route segments\n */\nexport type RoutePolyline = {\n    polyline: EncodedPolyline;\n    /**\n     * Unique route colors of routes containing this segment\n     */\n    colors: Array<(string)>;\n    /**\n     * Indexes into the top-level routes array for routes containing this segment\n     */\n    routeIndexes: Array<(number)>;\n};\n\nexport type RouteColor = {\n    color: string;\n    textColor: string;\n};\n\nexport type RoutePathSource = 'NONE' | 'TIMETABLE' | 'ROUTED';\n\nexport type TransitRouteInfo = {\n    id: string;\n    shortName: string;\n    longName: string;\n    color?: string;\n    textColor?: string;\n};\n\nexport type RouteInfo = {\n    /**\n     * Transport mode for this route\n     */\n    mode: Mode;\n    transitRoutes: Array<TransitRouteInfo>;\n    /**\n     * Number of stops along this route\n     */\n    numStops: number;\n    /**\n     * Internal route index for debugging purposes\n     */\n    routeIdx: number;\n    pathSource: RoutePathSource;\n    segments: Array<RouteSegment>;\n};\n\nexport type PlanData = {\n    query: {\n        /**\n         * Optional. Default is 0 minutes.\n         *\n         * Additional transfer time reserved for each transfer in minutes.\n         *\n         */\n        additionalTransferTime?: number;\n        /**\n         * algorithm to use\n         */\n        algorithm?: 'RAPTOR' | 'PONG' | 'TB';\n        /**\n         * Optional. Default is `false`.\n         *\n         * - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time\n         * - `arriveBy=false`: the parameters `date` and `time` refer to the departure time\n         *\n         */\n        arriveBy?: boolean;\n        /**\n         * Optional\n         *\n         * Average speed for bike routing.\n         *\n         */\n        cyclingSpeed?: CyclingSpeed;\n        /**\n         * Controls if `legGeometry` and `steps` are returned for direct legs,\n         * pre-/post-transit legs and transit legs.\n         *\n         */\n        detailedLegs?: boolean;\n        /**\n         * Controls if transfer polylines and step instructions are returned.\n         *\n         * If not set, this parameter inherits the value of `detailedLegs`.\n         *\n         * - true: Compute transfer polylines and step instructions.\n         * - false: Return empty `legGeometry` and omit `steps` for transfers.\n         *\n         */\n        detailedTransfers?: boolean;\n        /**\n         * Optional. Default is `WALK` which will compute walking routes as direct connections.\n         *\n         * Modes used for direction connections from start to destination without using transit.\n         * Results will be returned on the `direct` key.\n         *\n         * Note: Direct connections will only be returned on the first call. For paging calls, they can be omitted.\n         *\n         * Note: Transit connections that are slower than the fastest direct connection will not show up.\n         * This is being used as a cut-off during transit routing to speed up the search.\n         * To prevent this, it's possible to send two separate requests (one with only `transitModes` and one with only `directModes`).\n         *\n         * Note: the output `direct` array will stay empty if the input param `maxDirectTime` makes any direct trip impossible.\n         *\n         * Only non-transit modes such as `WALK`, `BIKE`, `CAR`, `BIKE_SHARING`, etc. can be used.\n         *\n         */\n        directModes?: Array<Mode>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies to direct connections.\n         *\n         * A list of vehicle type form factors that are allowed to be used for direct connections.\n         * If empty (the default), all form factors are allowed.\n         * Example: `BICYCLE,SCOOTER_STANDING`.\n         *\n         */\n        directRentalFormFactors?: Array<RentalFormFactor>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies to direct connections.\n         *\n         * A list of vehicle type form factors that are allowed to be used for direct connections.\n         * If empty (the default), all propulsion types are allowed.\n         * Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`.\n         *\n         */\n        directRentalPropulsionTypes?: Array<RentalPropulsionType>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies to direct connections.\n         *\n         * A list of rental provider groups that are allowed to be used for direct connections.\n         * If empty (the default), all providers are allowed.\n         *\n         */\n        directRentalProviderGroups?: Array<(string)>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies to direct connections.\n         *\n         * A list of rental providers that are allowed to be used for direct connections.\n         * If empty (the default), all providers are allowed.\n         *\n         */\n        directRentalProviders?: Array<(string)>;\n        /**\n         * Optional. Default is `NONE`.\n         *\n         * Set an elevation cost profile, to penalize routes with incline.\n         * - `NONE`: No additional costs for elevations. This is the default behavior\n         * - `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.\n         * - `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.\n         *\n         * As using an elevation costs profile will increase the travel duration,\n         * routing through steep terrain may exceed the maximal allowed duration,\n         * causing a location to appear unreachable.\n         * Increasing the maximum travel time for these segments may resolve this issue.\n         *\n         * The profile is used for direct routing, on the first mile, and last mile.\n         *\n         * Elevation cost profiles are currently used by following street modes:\n         * - `BIKE`\n         *\n         */\n        elevationCosts?: ElevationCosts;\n        /**\n         * Optional. Experimental. Default is `1.0`.\n         * Factor with which the duration of the fastest direct non-public-transit connection is multiplied.\n         * Values > 1.0 allow transit connections that are slower than the fastest direct non-public-transit connection to be found.\n         *\n         */\n        fastestDirectFactor?: number;\n        /**\n         * Optional.\n         * Factor with which the duration of the fastest slowDirect connection is multiplied.\n         * Values > 1.0 allow connections that are slower than the fastest direct transit connection to be found.\n         * Values < 1.0 will return all slowDirect connections.\n         *\n         */\n        fastestSlowDirectFactor?: number;\n        /**\n         * \\`latitude,longitude[,level]\\` tuple with\n         * - latitude and longitude in degrees\n         * - (optional) level: the OSM level (default: 0)\n         *\n         * OR\n         *\n         * stop id\n         *\n         */\n        fromPlace: string;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, the routing will ignore rental return constraints for direct connections,\n         * allowing the rental vehicle to be parked anywhere.\n         *\n         */\n        ignoreDirectRentalReturnConstraints?: boolean;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, the routing will ignore rental return constraints for the part from the last transit stop to the `to` coordinate,\n         * allowing the rental vehicle to be parked anywhere.\n         *\n         */\n        ignorePostTransitRentalReturnConstraints?: boolean;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, the routing will ignore rental return constraints for the part from the `from` coordinate to the first transit stop,\n         * allowing the rental vehicle to be parked anywhere.\n         *\n         */\n        ignorePreTransitRentalReturnConstraints?: boolean;\n        /**\n         * Optional. Default is `true`.\n         *\n         * Controls if a journey section with stay-seated transfers is returned:\n         * - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.).\n         * Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`.\n         * - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops\n         *\n         */\n        joinInterlinedLegs?: boolean;\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * Optional. Experimental. Number of luggage pieces; base unit: airline cabin luggage (e.g. for ODM or price calculation)\n         *\n         */\n        luggage?: number;\n        /**\n         * Optional. Default is 30min which is `1800`.\n         * Maximum time in seconds for direct connections.\n         * Is limited by server config variable `street_routing_max_direct_seconds`.\n         *\n         */\n        maxDirectTime?: number;\n        /**\n         * Optional. By default all computed itineraries will be returned\n         *\n         * The maximum number of itineraries to compute.\n         * This is only relevant if `timetableView=true`.\n         *\n         * Note: With the current implementation, setting this to a lower\n         * number will not result in any speedup.\n         *\n         * Note: The number of returned itineraries might be slightly higher\n         * than `maxItineraries` as there might be several itineraries with\n         * the same departure time but different number of transfers. In order\n         * to not miss any itineraries for paging, either none or all\n         * itineraries with the same departure time have to be returned.\n         *\n         */\n        maxItineraries?: number;\n        /**\n         * Optional. Default is 25 meters.\n         *\n         * Maximum matching distance in meters to match geo coordinates to the street network.\n         *\n         */\n        maxMatchingDistance?: number;\n        /**\n         * Optional. Default is 15min which is `900`.\n         * Maximum time in seconds for the last street leg.\n         * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n         *\n         */\n        maxPostTransitTime?: number;\n        /**\n         * Optional. Default is 15min which is `900`.\n         * Maximum time in seconds for the first street leg.\n         * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n         *\n         */\n        maxPreTransitTime?: number;\n        /**\n         * The maximum number of allowed transfers (i.e. interchanges between transit legs,\n         * pre- and postTransit do not count as transfers).\n         * `maxTransfers=0` searches for direct transit connections without any transfers.\n         * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n         * send an empty `transitModes` parameter instead.\n         *\n         * If not provided, the routing uses the server-side default value\n         * which is hardcoded and very high to cover all use cases.\n         *\n         * *Warning*: Use with care. Setting this too low can lead to\n         * optimal (e.g. the fastest) journeys not being found.\n         * If this value is too low to reach the destination at all,\n         * it can lead to slow routing performance.\n         *\n         * In plan endpoints before v3, the behavior is off by one,\n         * i.e. `maxTransfers=0` only returns non-transit connections.\n         *\n         */\n        maxTransfers?: number;\n        /**\n         * The maximum travel time in minutes.\n         * If not provided, the routing to uses the value\n         * hardcoded in the server which is usually quite high.\n         *\n         * *Warning*: Use with care. Setting this too low can lead to\n         * optimal (e.g. the least transfers) journeys not being found.\n         * If this value is too low to reach the destination at all,\n         * it can lead to slow routing performance.\n         *\n         */\n        maxTravelTime?: number;\n        /**\n         * Optional. Default is 0 minutes.\n         *\n         * Minimum transfer time for each transfer in minutes.\n         *\n         */\n        minTransferTime?: number;\n        /**\n         * The minimum number of itineraries to compute.\n         * This is only relevant if `timetableView=true`.\n         * The default value is 5.\n         *\n         */\n        numItineraries?: number;\n        /**\n         * Use the cursor to go to the next \"page\" of itineraries.\n         * Copy the cursor from the last response and keep the original request as is.\n         * This will enable you to search for itineraries in the next or previous time-window.\n         *\n         */\n        pageCursor?: string;\n        /**\n         * Optional. Experimental. Number of passengers (e.g. for ODM or price calculation)\n         */\n        passengers?: number;\n        /**\n         * Optional. Default is `FOOT`.\n         *\n         * Accessibility profile to use for pedestrian routing in transfers\n         * between transit connections, on the first mile, and last mile.\n         *\n         */\n        pedestrianProfile?: PedestrianProfile;\n        /**\n         * Optional\n         *\n         * Average speed for pedestrian routing.\n         *\n         */\n        pedestrianSpeed?: PedestrianSpeed;\n        /**\n         * 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`).\n         *\n         * A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.\n         *\n         */\n        postTransitModes?: Array<Mode>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`).\n         *\n         * A list of vehicle type form factors that are allowed to be used from the last transit stop to the `to` coordinate.\n         * If empty (the default), all form factors are allowed.\n         * Example: `BICYCLE,SCOOTER_STANDING`.\n         *\n         */\n        postTransitRentalFormFactors?: Array<RentalFormFactor>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`).\n         *\n         * A list of vehicle propulsion types that are allowed to be used from the last transit stop to the `to` coordinate.\n         * If empty (the default), all propulsion types are allowed.\n         * Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`.\n         *\n         */\n        postTransitRentalPropulsionTypes?: Array<RentalPropulsionType>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`).\n         *\n         * A list of rental provider groups that are allowed to be used from the last transit stop to the `to` coordinate.\n         * If empty (the default), all providers are allowed.\n         *\n         */\n        postTransitRentalProviderGroups?: Array<(string)>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `to` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`).\n         *\n         * A list of rental providers that are allowed to be used from the last transit stop to the `to` coordinate.\n         * If empty (the default), all providers are allowed.\n         *\n         */\n        postTransitRentalProviders?: Array<(string)>;\n        /**\n         * 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`).\n         *\n         * A list of modes that are allowed to be used from the `from` coordinate to the first transit stop. Example: `WALK,BIKE_SHARING`.\n         *\n         */\n        preTransitModes?: Array<Mode>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalFormFactors`).\n         *\n         * A list of vehicle type form factors that are allowed to be used from the `from` coordinate to the first transit stop.\n         * If empty (the default), all form factors are allowed.\n         * Example: `BICYCLE,SCOOTER_STANDING`.\n         *\n         */\n        preTransitRentalFormFactors?: Array<RentalFormFactor>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalPropulsionTypes`).\n         *\n         * A list of vehicle propulsion types that are allowed to be used from the `from` coordinate to the first transit stop.\n         * If empty (the default), all propulsion types are allowed.\n         * Example: `HUMAN,ELECTRIC,ELECTRIC_ASSIST`.\n         *\n         */\n        preTransitRentalPropulsionTypes?: Array<RentalPropulsionType>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviderGroups`).\n         *\n         * A list of rental provider groups that are allowed to be used from the `from` coordinate to the first transit stop.\n         * If empty (the default), all providers are allowed.\n         *\n         */\n        preTransitRentalProviderGroups?: Array<(string)>;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Only applies if the `from` place is a coordinate (not a transit stop). Does not apply to direct connections (see `directRentalProviders`).\n         *\n         * A list of rental providers that are allowed to be used from the `from` coordinate to the first transit stop.\n         * If empty (the default), all providers are allowed.\n         *\n         */\n        preTransitRentalProviders?: Array<(string)>;\n        /**\n         * Experimental. Search radius in meters around the `fromPlace` / `toPlace` coordinates.\n         * When set and the place is given as coordinates, all transit stops within\n         * this radius are used as start/end points with zero pre-transit/post-transit time.\n         * Works without OSM/street routing data loaded.\n         *\n         */\n        radius?: number;\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, all used transit trips are required to allow bike carriage.\n         *\n         */\n        requireBikeTransport?: boolean;\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, all used transit trips are required to allow car carriage.\n         *\n         */\n        requireCarTransport?: boolean;\n        /**\n         * Optional. Default is 15 minutes which is `900`.\n         *\n         * The length of the search-window in seconds. Default value 15 minutes.\n         *\n         * - `arriveBy=true`: number of seconds between the earliest departure time and latest departure time\n         * - `arriveBy=false`: number of seconds between the earliest arrival time and the latest arrival time\n         *\n         */\n        searchWindow?: number;\n        /**\n         * Optional. Experimental. Adds overtaken direct public transit connections.\n         */\n        slowDirect?: boolean;\n        /**\n         * Optional. Defaults to the current time.\n         *\n         * Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n         *\n         */\n        time?: string;\n        /**\n         * Optional. Query timeout in seconds.\n         */\n        timeout?: number;\n        /**\n         * Optional. Default is `true`.\n         *\n         * Search for the best trip options within a time window.\n         * If true two itineraries are considered optimal\n         * if one is better on arrival time (earliest wins)\n         * and the other is better on departure time (latest wins).\n         * In combination with arriveBy this parameter cover the following use cases:\n         *\n         * `timetable=false` = waiting for the first transit departure/arrival is considered travel time:\n         * - `arriveBy=true`: event (e.g. a meeting) starts at 10:00 am,\n         * compute the best journeys that arrive by that time (maximizes departure time)\n         * - `arriveBy=false`: event (e.g. a meeting) ends at 11:00 am,\n         * compute the best journeys that depart after that time\n         *\n         * `timetable=true` = optimize \"later departure\" + \"earlier arrival\" and give all options over a time window:\n         * - `arriveBy=true`: the time window around `date` and `time` refers to the arrival time window\n         * - `arriveBy=false`: the time window around `date` and `time` refers to the departure time window\n         *\n         */\n        timetableView?: boolean;\n        /**\n         * \\`latitude,longitude[,level]\\` tuple with\n         * - latitude and longitude in degrees\n         * - (optional) level: the OSM level (default: 0)\n         *\n         * OR\n         *\n         * stop id\n         *\n         */\n        toPlace: string;\n        /**\n         * Optional. Default is 1.0\n         *\n         * Factor to multiply minimum required transfer times with.\n         * Values smaller than 1.0 are not supported.\n         *\n         */\n        transferTimeFactor?: number;\n        /**\n         * Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n         * Allowed modes for the transit part. If empty, no transit connections will be computed.\n         * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n         *\n         */\n        transitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `false`.\n         *\n         * Whether to use transfers routed on OpenStreetMap data.\n         *\n         */\n        useRoutedTransfers?: boolean;\n        /**\n         * List of via stops to visit (only stop IDs, no coordinates allowed for now).\n         * Also see the optional parameter `viaMinimumStay` to set a set a minimum stay duration for each via stop.\n         *\n         */\n        via?: Array<(string)>;\n        /**\n         * Optional. If not set, the default is `0,0` - no stay required.\n         *\n         * For each `via` stop a minimum stay duration in minutes.\n         *\n         * The value `0` signals that it's allowed to stay in the same trip.\n         * This enables via stays without counting a transfer and can lead\n         * to better connections with less transfers. Transfer connections can\n         * still be found with `viaMinimumStay=0`.\n         *\n         */\n        viaMinimumStay?: Array<(number)>;\n        /**\n         * Optional. Experimental. If set to true, the response will contain fare information.\n         */\n        withFares?: boolean;\n        /**\n         * Optional. Include intermediate stops where passengers can not alight/board according to schedule.\n         */\n        withScheduledSkippedStops?: boolean;\n    };\n};\n\nexport type PlanResponse = ({\n    /**\n     * the routing query\n     */\n    requestParameters: {\n        [key: string]: (string);\n    };\n    /**\n     * debug statistics\n     */\n    debugOutput: {\n        [key: string]: (number);\n    };\n    from: Place;\n    to: Place;\n    /**\n     * Direct trips by `WALK`, `BIKE`, `CAR`, etc. without time-dependency.\n     * The starting time (`arriveBy=false`) / arrival time (`arriveBy=true`) is always the queried `time` parameter (set to \\\"now\\\" if not set).\n     * But all `direct` connections are meant to be independent of absolute times.\n     *\n     */\n    direct: Array<Itinerary>;\n    /**\n     * list of itineraries\n     */\n    itineraries: Array<Itinerary>;\n    /**\n     * Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page.\n     * 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.\n     *\n     */\n    previousPageCursor: string;\n    /**\n     * Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page.\n     * The next page is a set of itineraries departing AFTER the last itinerary in this result.\n     *\n     */\n    nextPageCursor: string;\n});\n\nexport type PlanError = (Error);\n\nexport type OneToManyData = {\n    query: {\n        /**\n         * true = many to one\n         * false = one to many\n         *\n         */\n        arriveBy: boolean;\n        /**\n         * Optional. Default is `NONE`.\n         *\n         * Set an elevation cost profile, to penalize routes with incline.\n         * - `NONE`: No additional costs for elevations. This is the default behavior\n         * - `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.\n         * - `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.\n         *\n         * As using an elevation costs profile will increase the travel duration,\n         * routing through steep terrain may exceed the maximal allowed duration,\n         * causing a location to appear unreachable.\n         * Increasing the maximum travel time for these segments may resolve this issue.\n         *\n         * Elevation cost profiles are currently used by following street modes:\n         * - `BIKE`\n         *\n         */\n        elevationCosts?: ElevationCosts;\n        /**\n         * geo locations as latitude;longitude,latitude;longitude,...\n         *\n         * The number of accepted locations is limited by server config variable `onetomany_max_many`.\n         *\n         */\n        many: Array<(string)>;\n        /**\n         * maximum travel time in seconds. Is limited by server config variable `street_routing_max_direct_seconds`.\n         */\n        max: number;\n        /**\n         * maximum matching distance in meters to match geo coordinates to the street network\n         */\n        maxMatchingDistance: number;\n        /**\n         * routing profile to use (currently supported: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`)\n         *\n         */\n        mode: Mode;\n        /**\n         * geo location as latitude;longitude\n         */\n        one: string;\n        /**\n         * Optional. Default is `false`.\n         * If true, the response includes the distance in meters\n         * for each path. This requires path reconstruction and\n         * is slower than duration-only queries.\n         *\n         */\n        withDistance?: boolean;\n    };\n};\n\nexport type OneToManyResponse = (Array<Duration>);\n\nexport type OneToManyError = (Error);\n\nexport type OneToManyPostData = {\n    body: OneToManyParams;\n};\n\nexport type OneToManyPostResponse = (Array<Duration>);\n\nexport type OneToManyPostError = (Error);\n\nexport type OneToManyIntermodalData = {\n    query: {\n        /**\n         * Optional. Default is 0 minutes.\n         *\n         * Additional transfer time reserved for each transfer in minutes.\n         *\n         */\n        additionalTransferTime?: number;\n        /**\n         * Optional. Defaults to false, i.e. one to many search\n         *\n         * true = many to one\n         * false = one to many\n         *\n         */\n        arriveBy?: boolean;\n        /**\n         * Optional\n         *\n         * Average speed for bike routing.\n         *\n         */\n        cyclingSpeed?: CyclingSpeed;\n        /**\n         * Default is `WALK` which will compute walking routes as direct connections.\n         *\n         * Mode used for direction connections from start to destination without using transit.\n         *\n         * Currently supported non-transit modes: \\`WALK\\`, \\`BIKE\\`, \\`CAR\\`\n         *\n         */\n        directMode?: Mode;\n        /**\n         * Optional. Default is `NONE`.\n         *\n         * Set an elevation cost profile, to penalize routes with incline.\n         * - `NONE`: No additional costs for elevations. This is the default behavior\n         * - `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.\n         * - `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.\n         *\n         * As using an elevation costs profile will increase the travel duration,\n         * routing through steep terrain may exceed the maximal allowed duration,\n         * causing a location to appear unreachable.\n         * Increasing the maximum travel time for these segments may resolve this issue.\n         *\n         * The profile is used for routing on both the first and last mile.\n         *\n         * Elevation cost profiles are currently used by following street modes:\n         * - `BIKE`\n         *\n         */\n        elevationCosts?: ElevationCosts;\n        /**\n         * geo locations as latitude;longitude,latitude;longitude,...\n         *\n         * The number of accepted locations is limited by server config variable `onetomany_max_many`.\n         *\n         */\n        many: Array<(string)>;\n        /**\n         * Optional. Default is 30min which is `1800`.\n         * Maximum time in seconds for direct connections.\n         *\n         * If a value smaller than either `maxPreTransitTime` or\n         * `maxPostTransitTime` is used, their maximum is set instead.\n         * Is limited by server config variable `street_routing_max_direct_seconds`.\n         *\n         */\n        maxDirectTime?: number;\n        /**\n         * maximum matching distance in meters to match geo coordinates to the street network\n         */\n        maxMatchingDistance?: number;\n        /**\n         * Optional. Default is 15min which is `900`.\n         * Maximum time in seconds for the last street leg.\n         * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n         *\n         */\n        maxPostTransitTime?: number;\n        /**\n         * Optional. Default is 15min which is `900`.\n         * Maximum time in seconds for the first street leg.\n         * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n         *\n         */\n        maxPreTransitTime?: number;\n        /**\n         * The maximum number of allowed transfers (i.e. interchanges between transit legs,\n         * pre- and postTransit do not count as transfers).\n         * `maxTransfers=0` searches for direct transit connections without any transfers.\n         * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n         * send an empty `transitModes` parameter instead.\n         *\n         * If not provided, the routing uses the server-side default value\n         * which is hardcoded and very high to cover all use cases.\n         *\n         * *Warning*: Use with care. Setting this too low can lead to\n         * optimal (e.g. the fastest) journeys not being found.\n         * If this value is too low to reach the destination at all,\n         * it can lead to slow routing performance.\n         *\n         */\n        maxTransfers?: number;\n        /**\n         * The maximum travel time in minutes.\n         * If not provided, the routing uses the value\n         * hardcoded in the server which is usually quite high.\n         *\n         * *Warning*: Use with care. Setting this too low can lead to\n         * optimal (e.g. the least transfers) journeys not being found.\n         * If this value is too low to reach the destination at all,\n         * it can lead to slow routing performance.\n         *\n         */\n        maxTravelTime?: number;\n        /**\n         * Optional. Default is 0 minutes.\n         *\n         * Minimum transfer time for each transfer in minutes.\n         *\n         */\n        minTransferTime?: number;\n        /**\n         * geo location as latitude;longitude\n         */\n        one: string;\n        /**\n         * Optional. Default is `FOOT`.\n         *\n         * Accessibility profile to use for pedestrian routing in transfers\n         * between transit connections and the first and last mile respectively.\n         *\n         */\n        pedestrianProfile?: PedestrianProfile;\n        /**\n         * Optional\n         *\n         * Average speed for pedestrian routing.\n         *\n         */\n        pedestrianSpeed?: PedestrianSpeed;\n        /**\n         * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n         *\n         * 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`.\n         *\n         */\n        postTransitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `WALK`. Does not apply to direct connections (see `directMode`).\n         *\n         * 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`.\n         *\n         */\n        preTransitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, all used transit trips are required to allow bike carriage.\n         *\n         */\n        requireBikeTransport?: boolean;\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, all used transit trips are required to allow car carriage.\n         *\n         */\n        requireCarTransport?: boolean;\n        /**\n         * Optional. Defaults to the current time.\n         *\n         * Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n         *\n         */\n        time?: string;\n        /**\n         * Optional. Default is 1.0\n         *\n         * Factor to multiply minimum required transfer times with.\n         * Values smaller than 1.0 are not supported.\n         *\n         */\n        transferTimeFactor?: number;\n        /**\n         * Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n         * Allowed modes for the transit part. If empty, no transit connections will be computed.\n         * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n         *\n         */\n        transitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `false`.\n         *\n         * Whether to use transfers routed on OpenStreetMap data.\n         *\n         */\n        useRoutedTransfers?: boolean;\n        /**\n         * Optional. Default is `false`.\n         * If true, the response includes the distance in meters\n         * for each path. This requires path reconstruction and\n         * is slower than duration-only queries.\n         *\n         * `withDistance` is currently limited to street routing.\n         *\n         */\n        withDistance?: boolean;\n    };\n};\n\nexport type OneToManyIntermodalResponse2 = (OneToManyIntermodalResponse);\n\nexport type OneToManyIntermodalError = (Error);\n\nexport type OneToManyIntermodalPostData = {\n    body: OneToManyIntermodalParams;\n};\n\nexport type OneToManyIntermodalPostResponse = (OneToManyIntermodalResponse);\n\nexport type OneToManyIntermodalPostError = (Error);\n\nexport type OneToAllData = {\n    query: {\n        /**\n         * Optional. Default is 0 minutes.\n         *\n         * Additional transfer time reserved for each transfer in minutes.\n         *\n         */\n        additionalTransferTime?: number;\n        /**\n         * true = all to one,\n         * false = one to all\n         *\n         */\n        arriveBy?: boolean;\n        /**\n         * Optional\n         *\n         * Average speed for bike routing.\n         *\n         */\n        cyclingSpeed?: CyclingSpeed;\n        /**\n         * Optional. Default is `NONE`.\n         *\n         * Set an elevation cost profile, to penalize routes with incline.\n         * - `NONE`: No additional costs for elevations. This is the default behavior\n         * - `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.\n         * - `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.\n         *\n         * As using an elevation costs profile will increase the travel duration,\n         * routing through steep terrain may exceed the maximal allowed duration,\n         * causing a location to appear unreachable.\n         * Increasing the maximum travel time for these segments may resolve this issue.\n         *\n         * The profile is used for routing on both the first and last mile.\n         *\n         * Elevation cost profiles are currently used by following street modes:\n         * - `BIKE`\n         *\n         */\n        elevationCosts?: ElevationCosts;\n        /**\n         * Optional. Default is 25 meters.\n         *\n         * Maximum matching distance in meters to match geo coordinates to the street network.\n         *\n         */\n        maxMatchingDistance?: number;\n        /**\n         * Optional. Default is 15min which is `900`.\n         * - `arriveBy=true`: Maximum time in seconds for the street leg at `one` location.\n         * - `arriveBy=false`: Currently not used\n         * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n         *\n         */\n        maxPostTransitTime?: number;\n        /**\n         * Optional. Default is 15min which is `900`.\n         * - `arriveBy=true`: Currently not used\n         * - `arriveBy=false`: Maximum time in seconds for the street leg at `one` location.\n         * Is limited by server config variable `street_routing_max_prepost_transit_seconds`.\n         *\n         */\n        maxPreTransitTime?: number;\n        /**\n         * The maximum number of allowed transfers (i.e. interchanges between transit legs,\n         * pre- and postTransit do not count as transfers).\n         * `maxTransfers=0` searches for direct transit connections without any transfers.\n         * If you want to search only for non-transit connections (`FOOT`, `CAR`, etc.),\n         * send an empty `transitModes` parameter instead.\n         *\n         * If not provided, the routing uses the server-side default value\n         * which is hardcoded and very high to cover all use cases.\n         *\n         * *Warning*: Use with care. Setting this too low can lead to\n         * optimal (e.g. the fastest) journeys not being found.\n         * If this value is too low to reach the destination at all,\n         * it can lead to slow routing performance.\n         *\n         * In plan endpoints before v3, the behavior is off by one,\n         * i.e. `maxTransfers=0` only returns non-transit connections.\n         *\n         */\n        maxTransfers?: number;\n        /**\n         * 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.\n         */\n        maxTravelTime: number;\n        /**\n         * Optional. Default is 0 minutes.\n         *\n         * Minimum transfer time for each transfer in minutes.\n         *\n         */\n        minTransferTime?: number;\n        /**\n         * \\`latitude,longitude[,level]\\` tuple with\n         * - latitude and longitude in degrees\n         * - (optional) level: the OSM level (default: 0)\n         *\n         * OR\n         *\n         * stop id\n         *\n         */\n        one: string;\n        /**\n         * Optional. Default is `FOOT`.\n         *\n         * Accessibility profile to use for pedestrian routing in transfers\n         * between transit connections and the first and last mile respectively.\n         *\n         */\n        pedestrianProfile?: PedestrianProfile;\n        /**\n         * Optional\n         *\n         * Average speed for pedestrian routing.\n         *\n         */\n        pedestrianSpeed?: PedestrianSpeed;\n        /**\n         * Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set:\n         * - `arriveBy=true`: Only applies if the `one` place is a coordinate (not a transit stop).\n         * - `arriveBy=false`: Currently not used\n         *\n         * A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.\n         *\n         */\n        postTransitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `WALK`. The behavior depends on whether `arriveBy` is set:\n         * - `arriveBy=true`: Currently not used\n         * - `arriveBy=false`: Only applies if the `one` place is a coordinate (not a transit stop).\n         *\n         * A list of modes that are allowed to be used from the last transit stop to the `to` coordinate. Example: `WALK,BIKE_SHARING`.\n         *\n         */\n        preTransitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, all used transit trips are required to allow bike carriage.\n         *\n         */\n        requireBikeTransport?: boolean;\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, all used transit trips are required to allow car carriage.\n         *\n         */\n        requireCarTransport?: boolean;\n        /**\n         * Optional. Defaults to the current time.\n         *\n         * Departure time ($arriveBy=false) / arrival date ($arriveBy=true),\n         *\n         */\n        time?: string;\n        /**\n         * Optional. Default is 1.0\n         *\n         * Factor to multiply minimum required transfer times with.\n         * Values smaller than 1.0 are not supported.\n         *\n         */\n        transferTimeFactor?: number;\n        /**\n         * Optional. Default is `TRANSIT` which allows all transit modes (no restriction).\n         * Allowed modes for the transit part. If empty, no transit connections will be computed.\n         * For example, this can be used to allow only `SUBURBAN,SUBWAY,TRAM`.\n         *\n         */\n        transitModes?: Array<Mode>;\n        /**\n         * Optional. Default is `false`.\n         *\n         * Whether to use transfers routed on OpenStreetMap data.\n         *\n         */\n        useRoutedTransfers?: boolean;\n    };\n};\n\nexport type OneToAllResponse = (Reachable);\n\nexport type OneToAllError = (Error);\n\nexport type ReverseGeocodeData = {\n    query: {\n        /**\n         * latitude, longitude in degrees\n         */\n        place: string;\n        /**\n         * Optional. Default is all types.\n         *\n         * Only return results of the given type.\n         * For example, this can be used to allow only `ADDRESS` and `STOP` results.\n         *\n         */\n        type?: LocationType;\n    };\n};\n\nexport type ReverseGeocodeResponse = (Array<Match>);\n\nexport type ReverseGeocodeError = (Error);\n\nexport type GeocodeData = {\n    query: {\n        /**\n         * language tags as used in OpenStreetMap\n         * (usually ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * Optional. Filter stops by available transport modes.\n         * Defaults to applying no filter.\n         *\n         */\n        mode?: Array<Mode>;\n        /**\n         * Optional. Used for biasing results towards the coordinate.\n         *\n         * Format: latitude,longitude in degrees\n         *\n         */\n        place?: string;\n        /**\n         * Optional. Used for biasing results towards the coordinate. Higher number = higher bias.\n         *\n         */\n        placeBias?: number;\n        /**\n         * the (potentially partially typed) address to resolve\n         */\n        text: string;\n        /**\n         * Optional. Default is all types.\n         *\n         * Only return results of the given types.\n         * For example, this can be used to allow only `ADDRESS` and `STOP` results.\n         *\n         */\n        type?: LocationType;\n    };\n};\n\nexport type GeocodeResponse = (Array<Match>);\n\nexport type GeocodeError = (Error);\n\nexport type TripData = {\n    query: {\n        /**\n         * Controls if `legGeometry` is returned for transit legs.\n         *\n         * The default value is `true`.\n         *\n         */\n        detailedLegs?: boolean;\n        /**\n         * Optional. Default is `true`.\n         *\n         * Controls if a trip with stay-seated transfers is returned:\n         * - `joinInterlinedLegs=false`: as several legs (full information about all trip numbers, headsigns, etc.).\n         * Legs that do not require a transfer (stay-seated transfer) are marked with `interlineWithPreviousLeg=true`.\n         * - `joinInterlinedLegs=true` (default behavior): as only one joined leg containing all stops\n         *\n         */\n        joinInterlinedLegs?: boolean;\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * trip identifier (e.g. from an itinerary leg or stop event)\n         */\n        tripId: string;\n        /**\n         * Optional. Include intermediate stops where passengers can not alight/board according to schedule.\n         */\n        withScheduledSkippedStops?: boolean;\n    };\n};\n\nexport type TripResponse = (Itinerary);\n\nexport type TripError = (Error);\n\nexport type StoptimesData = {\n    query?: {\n        /**\n         * Optional. Default is `false`.\n         *\n         * - `arriveBy=true`: the parameters `date` and `time` refer to the arrival time\n         * - `arriveBy=false`: the parameters `date` and `time` refer to the departure time\n         *\n         */\n        arriveBy?: boolean;\n        /**\n         * Anchor coordinate. Format: latitude,longitude pair.\n         * Used as fallback when \"stopId\" is missing or can't be found.\n         * If both are provided and \"stopId\" resolves, \"stopId\" is used.\n         * If \"stopId\" does not resolve, \"center\" is used instead. \"radius\" is\n         * required when querying by \"center\" (i.e. without a valid \"stopId\").\n         *\n         */\n        center?: string;\n        /**\n         * This parameter will be ignored in case `pageCursor` is set.\n         *\n         * Optional. Default is\n         * - `LATER` for `arriveBy=false`\n         * - `EARLIER` for `arriveBy=true`\n         *\n         * The response will contain the next `n` arrivals / departures\n         * in case `EARLIER` is selected and the previous `n`\n         * arrivals / departures if `LATER` is selected.\n         *\n         */\n        direction?: 'EARLIER' | 'LATER';\n        /**\n         * Optional. Default is `false`.\n         *\n         * If set to `true`, only stations that are phyiscally in the radius are considered.\n         * If set to `false`, additionally to the stations in the radius, equivalences with the same name and children are considered.\n         *\n         */\n        exactRadius?: boolean;\n        /**\n         * Experimental. Expect unannounced breaking changes (without version bumps).\n         *\n         * Optional. Default is `false`. If set to `true`, the following stops are returned\n         * for departures and the previous stops are returned for arrivals.\n         *\n         */\n        fetchStops?: boolean;\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * Optional. Default is all transit modes.\n         *\n         * Only return arrivals/departures of the given modes.\n         *\n         */\n        mode?: Array<Mode>;\n        /**\n         * Minimum number of events to return. If both `n` and `window`\n         * are provided, the API uses whichever returns more events.\n         *\n         */\n        n?: number;\n        /**\n         * Use the cursor to go to the next \"page\" of stop times.\n         * Copy the cursor from the last response and keep the original request as is.\n         * This will enable you to search for stop times in the next or previous time-window.\n         *\n         */\n        pageCursor?: string;\n        /**\n         * Optional. Radius in meters.\n         *\n         * Default is that only stop times of the parent of the stop itself\n         * and all stops with the same name (+ their child stops) are returned.\n         *\n         * If set, all stops at parent stations and their child stops in the specified radius\n         * are returned.\n         *\n         */\n        radius?: number;\n        /**\n         * stop id of the stop to retrieve departures/arrivals for\n         */\n        stopId?: string;\n        /**\n         * Optional. Defaults to the current time.\n         *\n         */\n        time?: string;\n        /**\n         * Optional. Window in seconds around `time`.\n         * Limiting the response to those that are at most `window` seconds aways in time.\n         * If both `n` and `window` are set, it uses whichever returns more.\n         *\n         */\n        window?: number;\n        /**\n         * Optional. Default is `true`. If set to `false`, alerts are omitted in the metadata of place for all stopTimes.\n         */\n        withAlerts?: boolean;\n        /**\n         * Optional. Include stoptimes where passengers can not alight/board according to schedule.\n         */\n        withScheduledSkippedStops?: boolean;\n    };\n};\n\nexport type StoptimesResponse = ({\n    /**\n     * list of stop times\n     */\n    stopTimes: Array<StopTime>;\n    /**\n     * metadata of the requested stop\n     */\n    place: Place;\n    /**\n     * Use the cursor to get the previous page of results. Insert the cursor into the request and post it to get the previous page.\n     * The previous page is a set of stop times BEFORE the first stop time in the result.\n     *\n     */\n    previousPageCursor: string;\n    /**\n     * Use the cursor to get the next page of results. Insert the cursor into the request and post it to get the next page.\n     * The next page is a set of stop times AFTER the last stop time in this result.\n     *\n     */\n    nextPageCursor: string;\n});\n\nexport type StoptimesError = (Error);\n\nexport type TripsData = {\n    query: {\n        /**\n         * end if the time window\n         */\n        endTime: string;\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * latitude,longitude pair of the upper left coordinate\n         */\n        max: string;\n        /**\n         * latitude,longitude pair of the lower right coordinate\n         */\n        min: string;\n        /**\n         * precision of returned polylines. Recommended to set based on zoom: `zoom >= 11 ? 5 : zoom >= 8 ? 4 : zoom >= 5 ? 3 : 2`\n         */\n        precision?: number;\n        /**\n         * start of the time window\n         */\n        startTime: string;\n        /**\n         * current zoom level\n         */\n        zoom: number;\n    };\n};\n\nexport type TripsResponse = (Array<TripSegment>);\n\nexport type TripsError = (Error);\n\nexport type InitialResponse = ({\n    /**\n     * latitude\n     */\n    lat: number;\n    /**\n     * longitude\n     */\n    lon: number;\n    /**\n     * zoom level\n     */\n    zoom: number;\n    serverConfig: ServerConfig;\n});\n\nexport type InitialError = (Error);\n\nexport type StopsData = {\n    query: {\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * latitude,longitude pair of the upper left coordinate\n         */\n        max: string;\n        /**\n         * latitude,longitude pair of the lower right coordinate\n         */\n        min: string;\n    };\n};\n\nexport type StopsResponse = (Array<Place>);\n\nexport type StopsError = (Error);\n\nexport type LevelsData = {\n    query: {\n        /**\n         * latitude,longitude pair of the upper left coordinate\n         */\n        max: string;\n        /**\n         * latitude,longitude pair of the lower right coordinate\n         */\n        min: string;\n    };\n};\n\nexport type LevelsResponse = (Array<(number)>);\n\nexport type LevelsError = (Error);\n\nexport type RoutesData = {\n    query: {\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * latitude,longitude pair of the upper left coordinate\n         */\n        max: string;\n        /**\n         * latitude,longitude pair of the lower right coordinate\n         */\n        min: string;\n        /**\n         * current zoom level\n         */\n        zoom: number;\n    };\n};\n\nexport type RoutesResponse = ({\n    routes: Array<RouteInfo>;\n    polylines: Array<RoutePolyline>;\n    stops: Array<Place>;\n    /**\n     * Indicates whether some routes were filtered out due to\n     * the zoom level.\n     *\n     */\n    zoomFiltered: boolean;\n});\n\nexport type RoutesError = (Error);\n\nexport type RouteDetailsData = {\n    query: {\n        /**\n         * language tags as used in OpenStreetMap / GTFS\n         * (usually BCP-47 / ISO 639-1, or ISO 639-2 if there's no ISO 639-1)\n         *\n         */\n        language?: Array<(string)>;\n        /**\n         * Internal route index\n         */\n        routeIdx: number;\n    };\n};\n\nexport type RouteDetailsResponse = ({\n    routes: Array<RouteInfo>;\n    polylines: Array<RoutePolyline>;\n    stops: Array<Place>;\n    /**\n     * Always false for this endpoint.\n     */\n    zoomFiltered: boolean;\n});\n\nexport type RouteDetailsError = (Error);\n\nexport type RentalsData = {\n    query?: {\n        /**\n         * latitude,longitude pair of the upper left coordinate\n         */\n        max?: string;\n        /**\n         * latitude,longitude pair of the lower right coordinate\n         */\n        min?: string;\n        /**\n         * \\`latitude,longitude[,level]\\` tuple with\n         * - latitude and longitude in degrees\n         * - (optional) level: the OSM level (ignored, for compatibility reasons)\n         *\n         * OR\n         *\n         * stop id\n         *\n         */\n        point?: string;\n        /**\n         * A list of rental provider groups to return.\n         * If both `providerGroups` and `providers` are empty/not specified,\n         * all providers in the map section are returned.\n         *\n         */\n        providerGroups?: Array<(string)>;\n        /**\n         * A list of rental providers to return.\n         * If both `providerGroups` and `providers` are empty/not specified,\n         * all providers in the map section are returned.\n         *\n         */\n        providers?: Array<(string)>;\n        /**\n         * Radius around `point` in meters.\n         *\n         */\n        radius?: number;\n        /**\n         * Optional. Include providers in output. If false, only provider\n         * groups are returned.\n         *\n         * Also affects the providers list for each provider group.\n         *\n         */\n        withProviders?: boolean;\n        /**\n         * Optional. Include stations in output (requires at least min+max or providers filter).\n         */\n        withStations?: boolean;\n        /**\n         * Optional. Include free-floating vehicles in output (requires at least min+max or providers filter).\n         */\n        withVehicles?: boolean;\n        /**\n         * Optional. Include geofencing zones in output (requires at least min+max or providers filter).\n         */\n        withZones?: boolean;\n    };\n};\n\nexport type RentalsResponse = ({\n    providerGroups: Array<RentalProviderGroup>;\n    providers: Array<RentalProvider>;\n    stations: Array<RentalStation>;\n    vehicles: Array<RentalVehicle>;\n    zones: Array<RentalZone>;\n});\n\nexport type RentalsError = (Error);\n\nexport type TransfersData = {\n    query: {\n        /**\n         * location id\n         */\n        id: string;\n    };\n};\n\nexport type TransfersResponse = ({\n    place: Place;\n    root: Place;\n    equivalences: Array<Place>;\n    /**\n     * true if the server has foot transfers computed\n     */\n    hasFootTransfers: boolean;\n    /**\n     * true if the server has wheelchair transfers computed\n     */\n    hasWheelchairTransfers: boolean;\n    /**\n     * true if the server has car transfers computed\n     */\n    hasCarTransfers: boolean;\n    /**\n     * all outgoing transfers of this location\n     */\n    transfers: Array<Transfer>;\n});\n\nexport type TransfersError = unknown;"
  },
  {
    "path": "ui/api/package.json",
    "content": "{\n\t\"name\": \"@motis-project/motis-client\",\n\t\"version\": \"2.0.0\",\n\t\"description\": \"A JS client for the MOTIS API.\",\n\t\"public\": true,\n\t\"main\": \"dist/index.js\",\n\t\"types\": \"dist/index.d.ts\",\n\t\"files\": [\n\t\t\"/dist\"\n\t],\n\t\"scripts\": {\n\t\t\"generate\": \"openapi-ts -i ../../openapi.yaml -o ./openapi/ -c @hey-api/client-fetch\",\n\t\t\"transpile\": \"tsup openapi/**/*.ts --format esm --dts -d=./dist\",\n\t\t\"build\": \"pnpm generate && pnpm transpile\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/motis-project/motis.git\"\n\t},\n\t\"author\": \"motis-project\",\n\t\"license\": \"MIT\",\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/motis-project/motis/issues\"\n\t},\n\t\"homepage\": \"https://github.com/motis-project/motis#readme\",\n\t\"devDependencies\": {\n\t\t\"@hey-api/openapi-ts\": \"^0.53.12\",\n\t\t\"tslib\": \"^2.8.1\",\n\t\t\"tsup\": \"^8.4.0\",\n\t\t\"typescript\": \"^5.7.3\"\n\t},\n\t\"dependencies\": {\n\t\t\"@hey-api/client-fetch\": \"^0.4.4\"\n\t},\n\t\"type\": \"module\"\n}\n"
  },
  {
    "path": "ui/api/tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t/* Visit https://aka.ms/tsconfig to read more about this file */\n\n\t\t/* Projects */\n\t\t// \"incremental\": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */\n\t\t// \"composite\": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */\n\t\t// \"tsBuildInfoFile\": \"./.tsbuildinfo\",              /* Specify the path to .tsbuildinfo incremental compilation file. */\n\t\t// \"disableSourceOfProjectReferenceRedirect\": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */\n\t\t// \"disableSolutionSearching\": true,                 /* Opt a project out of multi-project reference checking when editing. */\n\t\t// \"disableReferencedProjectLoad\": true,             /* Reduce the number of projects loaded automatically by TypeScript. */\n\n\t\t/* Language and Environment */\n\t\t\"target\": \"ESNext\" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,\n\t\t// \"lib\": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */\n\t\t// \"jsx\": \"preserve\",                                /* Specify what JSX code is generated. */\n\t\t// \"libReplacement\": true,                           /* Enable lib replacement. */\n\t\t// \"experimentalDecorators\": true,                   /* Enable experimental support for legacy experimental decorators. */\n\t\t// \"emitDecoratorMetadata\": true,                    /* Emit design-type metadata for decorated declarations in source files. */\n\t\t// \"jsxFactory\": \"\",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */\n\t\t// \"jsxFragmentFactory\": \"\",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */\n\t\t// \"jsxImportSource\": \"\",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */\n\t\t// \"reactNamespace\": \"\",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */\n\t\t// \"noLib\": true,                                    /* Disable including any library files, including the default lib.d.ts. */\n\t\t// \"useDefineForClassFields\": true,                  /* Emit ECMAScript-standard-compliant class fields. */\n\t\t// \"moduleDetection\": \"auto\",                        /* Control what method is used to detect module-format JS files. */\n\n\t\t/* Modules */\n\t\t\"module\": \"ESNext\" /* Specify what module code is generated. */,\n\t\t// \"rootDir\": \"./\",                                  /* Specify the root folder within your source files. */\n\t\t\"moduleResolution\": \"bundler\" /* Specify how TypeScript looks up a file from a given module specifier. */,\n\t\t// \"baseUrl\": \"./\",                                  /* Specify the base directory to resolve non-relative module names. */\n\t\t// \"paths\": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */\n\t\t// \"rootDirs\": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */\n\t\t// \"typeRoots\": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */\n\t\t// \"types\": [],                                      /* Specify type package names to be included without being referenced in a source file. */\n\t\t// \"allowUmdGlobalAccess\": true,                     /* Allow accessing UMD globals from modules. */\n\t\t// \"moduleSuffixes\": [],                             /* List of file name suffixes to search when resolving a module. */\n\t\t// \"allowImportingTsExtensions\": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */\n\t\t// \"rewriteRelativeImportExtensions\": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */\n\t\t// \"resolvePackageJsonExports\": true,                /* Use the package.json 'exports' field when resolving package imports. */\n\t\t// \"resolvePackageJsonImports\": true,                /* Use the package.json 'imports' field when resolving imports. */\n\t\t// \"customConditions\": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */\n\t\t// \"noUncheckedSideEffectImports\": true,             /* Check side effect imports. */\n\t\t// \"resolveJsonModule\": true,                        /* Enable importing .json files. */\n\t\t// \"allowArbitraryExtensions\": true,                 /* Enable importing files with any extension, provided a declaration file is present. */\n\t\t// \"noResolve\": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */\n\n\t\t/* JavaScript Support */\n\t\t// \"allowJs\": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */\n\t\t// \"checkJs\": true,                                  /* Enable error reporting in type-checked JavaScript files. */\n\t\t// \"maxNodeModuleJsDepth\": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */\n\n\t\t/* Emit */\n\t\t\"declaration\": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,\n\t\t// \"declarationMap\": true,                           /* Create sourcemaps for d.ts files. */\n\t\t// \"emitDeclarationOnly\": true,                      /* Only output d.ts files and not JavaScript files. */\n\t\t\"sourceMap\": true /* Create source map files for emitted JavaScript files. */,\n\t\t// \"inlineSourceMap\": true,                          /* Include sourcemap files inside the emitted JavaScript. */\n\t\t// \"noEmit\": true,                                   /* Disable emitting files from a compilation. */\n\t\t// \"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. */\n\t\t\"outDir\": \"./dist/\" /* Specify an output folder for all emitted files. */,\n\t\t// \"removeComments\": true,                           /* Disable emitting comments. */\n\t\t// \"importHelpers\": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */\n\t\t// \"downlevelIteration\": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */\n\t\t// \"sourceRoot\": \"\",                                 /* Specify the root path for debuggers to find the reference source code. */\n\t\t// \"mapRoot\": \"\",                                    /* Specify the location where debugger should locate map files instead of generated locations. */\n\t\t// \"inlineSources\": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */\n\t\t// \"emitBOM\": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */\n\t\t// \"newLine\": \"crlf\",                                /* Set the newline character for emitting files. */\n\t\t// \"stripInternal\": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */\n\t\t// \"noEmitHelpers\": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */\n\t\t// \"noEmitOnError\": true,                            /* Disable emitting files if any type checking errors are reported. */\n\t\t// \"preserveConstEnums\": true,                       /* Disable erasing 'const enum' declarations in generated code. */\n\t\t// \"declarationDir\": \"./\",                           /* Specify the output directory for generated declaration files. */\n\n\t\t/* Interop Constraints */\n\t\t// \"isolatedModules\": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */\n\t\t// \"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. */\n\t\t// \"isolatedDeclarations\": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */\n\t\t// \"erasableSyntaxOnly\": true,                       /* Do not allow runtime constructs that are not part of ECMAScript. */\n\t\t// \"allowSyntheticDefaultImports\": true,             /* Allow 'import x from y' when a module doesn't have a default export. */\n\t\t\"esModuleInterop\": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,\n\t\t// \"preserveSymlinks\": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */\n\t\t\"forceConsistentCasingInFileNames\": true /* Ensure that casing is correct in imports. */,\n\n\t\t/* Type Checking */\n\t\t\"strict\": true /* Enable all strict type-checking options. */,\n\t\t// \"noImplicitAny\": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */\n\t\t// \"strictNullChecks\": true,                         /* When type checking, take into account 'null' and 'undefined'. */\n\t\t// \"strictFunctionTypes\": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */\n\t\t// \"strictBindCallApply\": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */\n\t\t// \"strictPropertyInitialization\": true,             /* Check for class properties that are declared but not set in the constructor. */\n\t\t// \"strictBuiltinIteratorReturn\": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */\n\t\t// \"noImplicitThis\": true,                           /* Enable error reporting when 'this' is given the type 'any'. */\n\t\t// \"useUnknownInCatchVariables\": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */\n\t\t// \"alwaysStrict\": true,                             /* Ensure 'use strict' is always emitted. */\n\t\t// \"noUnusedLocals\": true,                           /* Enable error reporting when local variables aren't read. */\n\t\t// \"noUnusedParameters\": true,                       /* Raise an error when a function parameter isn't read. */\n\t\t// \"exactOptionalPropertyTypes\": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */\n\t\t// \"noImplicitReturns\": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */\n\t\t// \"noFallthroughCasesInSwitch\": true,               /* Enable error reporting for fallthrough cases in switch statements. */\n\t\t// \"noUncheckedIndexedAccess\": true,                 /* Add 'undefined' to a type when accessed using an index. */\n\t\t// \"noImplicitOverride\": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */\n\t\t// \"noPropertyAccessFromIndexSignature\": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */\n\t\t// \"allowUnusedLabels\": true,                        /* Disable error reporting for unused labels. */\n\t\t// \"allowUnreachableCode\": true,                     /* Disable error reporting for unreachable code. */\n\n\t\t/* Completeness */\n\t\t// \"skipDefaultLibCheck\": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */\n\t\t\"skipLibCheck\": true /* Skip type checking all .d.ts files. */\n\t}\n}\n"
  },
  {
    "path": "ui/components.json",
    "content": "{\n\t\"$schema\": \"https://next.shadcn-svelte.com/schema.json\",\n\t\"style\": \"new-york\",\n\t\"tailwind\": {\n\t\t\"config\": \"tailwind.config.js\",\n\t\t\"css\": \"src/app.css\",\n\t\t\"baseColor\": \"slate\"\n\t},\n\t\"aliases\": {\n\t\t\"components\": \"$lib/components\",\n\t\t\"utils\": \"$lib/utils\",\n\t\t\"ui\": \"$lib/components/ui\",\n\t\t\"hooks\": \"$lib/hooks\"\n\t},\n\t\"typescript\": true,\n\t\"registry\": \"https://next.shadcn-svelte.com/registry\"\n}\n"
  },
  {
    "path": "ui/eslint.config.js",
    "content": "import prettier from 'eslint-config-prettier';\nimport js from '@eslint/js';\nimport { includeIgnoreFile } from '@eslint/compat';\nimport svelte from 'eslint-plugin-svelte';\nimport globals from 'globals';\nimport { fileURLToPath } from 'node:url';\nimport ts from 'typescript-eslint';\nconst gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));\n\nexport default ts.config(\n\tincludeIgnoreFile(gitignorePath),\n\tjs.configs.recommended,\n\t...ts.configs.recommended,\n\t...svelte.configs['flat/recommended'],\n\tprettier,\n\t...svelte.configs['flat/prettier'],\n\t{\n\t\tlanguageOptions: {\n\t\t\tglobals: {\n\t\t\t\t...globals.browser,\n\t\t\t\t...globals.node\n\t\t\t}\n\t\t}\n\t},\n\t{\n\t\tfiles: ['**/*.svelte'],\n\n\t\tlanguageOptions: {\n\t\t\tparserOptions: {\n\t\t\t\tparser: ts.parser\n\t\t\t}\n\t\t}\n\t},\n\t{\n\t\trules: {\n\t\t\t'@typescript-eslint/no-unused-vars': [\n\t\t\t\t'error',\n\t\t\t\t{\n\t\t\t\t\targs: 'all',\n\t\t\t\t\targsIgnorePattern: '^_',\n\t\t\t\t\tcaughtErrors: 'all',\n\t\t\t\t\tcaughtErrorsIgnorePattern: '^_',\n\t\t\t\t\tdestructuredArrayIgnorePattern: '^_',\n\t\t\t\t\tvarsIgnorePattern: '^_',\n\t\t\t\t\tignoreRestSiblings: true\n\t\t\t\t}\n\t\t\t],\n\t\t\t'svelte/no-navigation-without-resolve': 'off'\n\t\t}\n\t}\n);\n"
  },
  {
    "path": "ui/package.json",
    "content": "{\n\t\"name\": \"ui\",\n\t\"version\": \"0.0.1\",\n\t\"private\": true,\n\t\"scripts\": {\n\t\t\"prepare\": \"svelte-kit sync\",\n\t\t\"dev\": \"vite dev\",\n\t\t\"build\": \"vite build\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"test\": \"npm run test:integration && npm run test:unit\",\n\t\t\"check\": \"svelte-check --tsconfig ./tsconfig.json\",\n\t\t\"check:watch\": \"svelte-check --tsconfig ./tsconfig.json --watch\",\n\t\t\"lint\": \"prettier --check . && eslint .\",\n\t\t\"format\": \"prettier --write .\",\n\t\t\"test:integration\": \"playwright test\",\n\t\t\"test:unit\": \"vitest\",\n\t\t\"update-sprite\": \"spreet sprite --sdf static/sprite_sdf && spreet --sdf --retina sprite static/sprite_sdf@2x\",\n\t\t\"update-api\": \"pnpm --filter @motis-project/motis-client build\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@eslint/compat\": \"^1.4.1\",\n\t\t\"@eslint/eslintrc\": \"^3.3.3\",\n\t\t\"@eslint/js\": \"^9.39.2\",\n\t\t\"@hey-api/openapi-ts\": \"^0.53.12\",\n\t\t\"@playwright/test\": \"^1.58.2\",\n\t\t\"@sveltejs/adapter-static\": \"^3.0.10\",\n\t\t\"@sveltejs/kit\": \"^2.50.2\",\n\t\t\"@sveltejs/vite-plugin-svelte\": \"^6.2.4\",\n\t\t\"@types/earcut\": \"^3.0.0\",\n\t\t\"@types/eslint\": \"^9.6.1\",\n\t\t\"@types/estree\": \"^1.0.8\",\n\t\t\"@types/geojson\": \"^7946.0.16\",\n\t\t\"@types/mapbox__polyline\": \"^1.0.5\",\n\t\t\"@types/rbush\": \"^4.0.0\",\n\t\t\"@typescript-eslint/eslint-plugin\": \"^8.55.0\",\n\t\t\"@typescript-eslint/parser\": \"^8.55.0\",\n\t\t\"autoprefixer\": \"^10.4.24\",\n\t\t\"bits-ui\": \"^1.8.0\",\n\t\t\"clsx\": \"^2.1.1\",\n\t\t\"eslint\": \"^9.39.2\",\n\t\t\"eslint-config-prettier\": \"^10.1.8\",\n\t\t\"eslint-plugin-svelte\": \"^3.14.0\",\n\t\t\"globals\": \"^16.5.0\",\n\t\t\"postcss\": \"^8.5.6\",\n\t\t\"prettier\": \"^3.8.1\",\n\t\t\"prettier-plugin-svelte\": \"^3.4.1\",\n\t\t\"svelte\": \"^5.50.1\",\n\t\t\"svelte-check\": \"^4.3.6\",\n\t\t\"tailwind-merge\": \"^2.6.1\",\n\t\t\"tailwind-variants\": \"^0.3.1\",\n\t\t\"tailwindcss\": \"^3.4.19\",\n\t\t\"tailwindcss-animate\": \"^1.0.7\",\n\t\t\"tslib\": \"^2.8.1\",\n\t\t\"typescript\": \"^5.9.3\",\n\t\t\"typescript-eslint\": \"^8.55.0\",\n\t\t\"vite\": \"^7.3.1\",\n\t\t\"vitest\": \"^4.0.18\"\n\t},\n\t\"type\": \"module\",\n\t\"dependencies\": {\n\t\t\"@deck.gl/core\": \"~9.2.6\",\n\t\t\"@deck.gl/layers\": \"~9.2.6\",\n\t\t\"@deck.gl/mapbox\": \"~9.2.6\",\n\t\t\"@hey-api/client-fetch\": \"^0.4.4\",\n\t\t\"@lucide/svelte\": \"^0.548.0\",\n\t\t\"@mapbox/polyline\": \"^1.2.1\",\n\t\t\"@motis-project/motis-client\": \"workspace:^\",\n\t\t\"@tanstack/svelte-query\": \"^6.0.18\",\n\t\t\"@turf/boolean-point-in-polygon\": \"^7.3.4\",\n\t\t\"@turf/circle\": \"^7.3.4\",\n\t\t\"@turf/destination\": \"^7.3.4\",\n\t\t\"@turf/difference\": \"^7.3.4\",\n\t\t\"@turf/helpers\": \"^7.3.4\",\n\t\t\"@turf/rhumb-bearing\": \"^7.3.4\",\n\t\t\"@turf/rhumb-distance\": \"^7.3.4\",\n\t\t\"@turf/union\": \"^7.3.4\",\n\t\t\"colord\": \"^2.9.3\",\n\t\t\"earcut\": \"^3.0.2\",\n\t\t\"geojson\": \"^0.5.0\",\n\t\t\"maplibre-gl\": \"^5.17.0\",\n\t\t\"rbush\": \"^4.0.1\",\n\t\t\"svelte-radix\": \"^1.1.1\"\n\t},\n\t\"devEngines\": {\n\t\t\"packageManager\": {\n\t\t\t\"name\": \"pnpm\",\n\t\t\t\"onFail\": \"error\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ui/playwright.config.ts",
    "content": "import type { PlaywrightTestConfig } from '@playwright/test';\n\nconst config: PlaywrightTestConfig = {\n\twebServer: {\n\t\tcommand: 'npm run build && npm run preview',\n\t\tport: 4173\n\t},\n\ttestDir: 'tests',\n\ttestMatch: /(.+\\.)?(test|spec)\\.[jt]s/\n};\n\nexport default config;\n"
  },
  {
    "path": "ui/pnpm-workspace.yaml",
    "content": "packages:\n  - api\n\nincludeWorkspaceRoot: true\n\nonlyBuiltDependencies:\n  - esbuild\n"
  },
  {
    "path": "ui/postcss.config.js",
    "content": "export default {\n\tplugins: {\n\t\ttailwindcss: {},\n\t\tautoprefixer: {}\n\t}\n};\n"
  },
  {
    "path": "ui/src/app.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n\t:root {\n\t\t--background: 0 0% 100%;\n\t\t--foreground: 240 10% 3.9%;\n\t\t--muted: 240 4.8% 95.9%;\n\t\t--muted-foreground: 240 3.8% 46.1%;\n\t\t--popover: 0 0% 100%;\n\t\t--popover-foreground: 240 10% 3.9%;\n\t\t--card: 0 0% 100%;\n\t\t--card-foreground: 240 10% 3.9%;\n\t\t--border: 240 5.9% 90%;\n\t\t--input: 240 5.9% 90%;\n\t\t--primary: 240 5.9% 10%;\n\t\t--primary-foreground: 0 0% 98%;\n\t\t--secondary: 240 4.8% 95.9%;\n\t\t--secondary-foreground: 240 5.9% 10%;\n\t\t--accent: 240 4.8% 95.9%;\n\t\t--accent-foreground: 240 5.9% 10%;\n\t\t--destructive: 0 72.2% 50.6%;\n\t\t--destructive-foreground: 0 0% 98%;\n\t\t--ring: 240 10% 3.9%;\n\t\t--radius: 0.5rem;\n\t\t--sidebar-background: 0 0% 98%;\n\t\t--sidebar-foreground: 240 5.3% 26.1%;\n\t\t--sidebar-primary: 240 5.9% 10%;\n\t\t--sidebar-primary-foreground: 0 0% 98%;\n\t\t--sidebar-accent: 240 4.8% 95.9%;\n\t\t--sidebar-accent-foreground: 240 5.9% 10%;\n\t\t--sidebar-border: 220 13% 91%;\n\t\t--sidebar-ring: 217.2 91.2% 59.8%;\n\t}\n\n\t.dark {\n\t\t--background: 240 10% 3.9%;\n\t\t--foreground: 0 0% 98%;\n\t\t--muted: 240 3.7% 15.9%;\n\t\t--muted-foreground: 240 5% 64.9%;\n\t\t--popover: 240 10% 3.9%;\n\t\t--popover-foreground: 0 0% 98%;\n\t\t--card: 240 10% 3.9%;\n\t\t--card-foreground: 0 0% 98%;\n\t\t--border: 240 3.7% 15.9%;\n\t\t--input: 240 3.7% 15.9%;\n\t\t--primary: 0 0% 98%;\n\t\t--primary-foreground: 240 5.9% 10%;\n\t\t--secondary: 240 3.7% 15.9%;\n\t\t--secondary-foreground: 0 0% 98%;\n\t\t--accent: 240 3.7% 15.9%;\n\t\t--accent-foreground: 0 0% 98%;\n\t\t--destructive: 0 62.8% 45.6%;\n\t\t--destructive-foreground: 0 0% 98%;\n\t\t--ring: 240 4.9% 83.9%;\n\t\t--sidebar-background: 240 5.9% 10%;\n\t\t--sidebar-foreground: 240 4.8% 95.9%;\n\t\t--sidebar-primary: 224.3 76.3% 48%;\n\t\t--sidebar-primary-foreground: 0 0% 100%;\n\t\t--sidebar-accent: 240 3.7% 15.9%;\n\t\t--sidebar-accent-foreground: 240 4.8% 95.9%;\n\t\t--sidebar-border: 240 3.7% 15.9%;\n\t\t--sidebar-ring: 217.2 91.2% 59.8%;\n\t}\n}\n\n@layer base {\n\t* {\n\t\t@apply border-border;\n\t}\n\n\tbody {\n\t\t@apply bg-secondary text-foreground;\n\t}\n}\n\nbody {\n\tfont:\n\t\t12px / 20px Helvetica Neue,\n\t\tArial,\n\t\tHelvetica,\n\t\tsans-serif;\n}\n\n.maplibregl-popup-anchor-top .maplibregl-popup-tip,\n.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,\n.maplibregl-popup-anchor-top-right .maplibregl-popup-tip {\n\tborder-bottom-color: hsl(var(--background)) !important;\n}\n\n.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,\n.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,\n.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {\n\tborder-top-color: hsl(var(--background)) !important;\n}\n\n.maplibregl-popup-anchor-left .maplibregl-popup-tip {\n\tborder-right-color: hsl(var(--background)) !important;\n}\n\n.maplibregl-popup-anchor-right .maplibregl-popup-tip {\n\tborder-left-color: hsl(var(--background)) !important;\n}\n\n.maplibregl-popup-content {\n\tbackground-color: hsl(var(--background)) !important;\n\tpadding: 12px 40px 12px 12px !important;\n}\n\n.maplibregl-popup-close-button {\n\tfont-size: 24px !important;\n\twidth: 32px !important;\n\theight: 32px !important;\n\tline-height: 32px !important;\n\tpadding: 0 !important;\n\tdisplay: flex !important;\n\talign-items: center !important;\n\tjustify-content: center !important;\n\tcolor: hsl(var(--foreground)) !important;\n\topacity: 0.7;\n\ttransition: opacity 0.2s;\n}\n\n.maplibregl-popup-close-button:hover {\n\tbackground-color: hsl(var(--accent)) !important;\n\topacity: 1;\n}\n\n.maplibregl-control-container .maplibregl-ctrl-top-left {\n\tmax-width: 100%;\n\tbottom: 0.5rem;\n\tdisplay: flex;\n\tflex-direction: column;\n\tz-index: 3;\n}\n\n.maplibregl-ctrl-group .maplibregl-ctrl-geolocate {\n\tdisplay: none;\n}\n\n.maplibregl-ctrl-top-left .maplibregl-ctrl.maplibregl-ctrl-scale {\n\tmargin-top: 25px;\n}\n\n.hide {\n\tdisplay: none;\n}\nhtml,\nbody {\n\toverscroll-behavior: none;\n}\n"
  },
  {
    "path": "ui/src/app.d.ts",
    "content": "// See https://kit.svelte.dev/docs/types#app\n\nimport type { Itinerary } from '@motis-project/motis-client';\n\n// for information about these interfaces\ndeclare global {\n\tnamespace App {\n\t\t// interface Error {}\n\t\t// interface Locals {}\n\t\t// interface PageData {}\n\t\tinterface PageState {\n\t\t\tselectedItinerary?: Itinerary;\n\t\t\tselectedStop?: { name: string; stopId: string; time: Date };\n\t\t\tstopArriveBy?: boolean;\n\t\t\ttripId?: string;\n\t\t\tactiveTab?: 'connections' | 'departures' | 'isochrones';\n\t\t\tscrollY?: number;\n\t\t}\n\t\t// interface Platform {}\n\t}\n}\n\nexport {};\n"
  },
  {
    "path": "ui/src/app.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>MOTIS</title>\n\t\t<link rel=\"icon\" href=\"%sveltekit.assets%/favicon.ico\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t%sveltekit.head%\n\t</head>\n\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">%sveltekit.body%</div>\n\n\t\t<svg style=\"display: none\">\n\t\t\t<symbol id=\"train\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M4,15.5C4,17.43 5.57,19 7.5,19L6,20.5v0.5h12v-0.5L16.5,19c1.93,0 3.5,-1.57 3.5,-3.5L20,5c0,-3.5 -3.58,-4 -8,-4s-8,0.5 -8,4v10.5zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM18,10L6,10L6,5h12v5z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"sbahn\" viewBox=\"0 0 64 64\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M 23.9104,17.8176 c 0,-2.7648 2.5344,-5.1456 7.0144,-5.1456 7.8592,0\n\t\t14.5408,4.224 19.0208,9.0368 l 0,-8.3968 C 44.8,9.3184 38.2976,6.9376\n\t\t31.0784,6.9376 c -8.6784,0 -18.3296,5.3248 -18.3296,15.232 0,18.8672\n\t\t28.6208,12.1088 28.6208,23.0144 0,2.9184 -3.7888,5.632 -8.576,5.632 -7.7312,0\n\t\t-15.4368,-4.7104 -19.6608,-10.7776 l 0,10.0864 c 4.48,3.9936 12.6208,7.168\n\t\t19.6608,7.168 12.672,0 20.1984,-9.0368 20.1984,-16.768 0,-19.2512\n\t\t-29.0816,-11.008 -29.0816,-22.7072 z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"ubahn\" viewBox=\"0 0 500 500\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M269.02771 462.03556C301.49095 458.86693 331.81940 448.53888 355.90324\n\t\t432.45098C368.00616 424.36627 385.07594 407.62587 392.49694 396.56338C403.34218\n\t\t380.39638 411.21817 360.97199 415.43165 340C417.06374 331.87653 417.26314\n\t\t317.60376 417.59707 185L417.96486 38.947368L317.89474 38.947368L317.89474\n\t\t172.67306C317.89474 260.58200 317.50921 309.85624 316.76948 316.49013C315.44428\n\t\t328.37460 311.53547 342.57976 307.56662 349.93462C302.58644 359.16362 293.39802\n\t\t367.94547 283.81131 372.63882C257.43623 385.55122 218.84256 380.90205 200.54920\n\t\t362.60869C191.23263 353.29213 184.83222 338.29015 182.14617 319.47368C181.48964\n\t\t314.87452 181.08649 260.77515 181.07348 175.52632L181.05263 38.947368L82.105263\n\t\t38.947368L82.105263 177.25689C82.105263 316.01926 82.371267 327.73304 85.920452\n\t\t345.26316C94.686023 388.55802 121.69624 423.01667 163.04724 443.65869C193.35569\n\t\t458.78839 232.59204 465.59192 269.02771 462.03556Z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"tram\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M17,18C16.4,18 16,17.6 16,17C16,16.4 16.4,16 17,16C17.6,16 18,16.4 18,17C18,17.6 17.6,18 17,18M6.7,10.7L7,7.3C7,6.6 7.6,6 8.3,6H15.6C16.4,6 17,6.6 17,7.3L17.3,10.6C17.3,11.3 16.7,11.9 16,11.9H8C7.3,12 6.7,11.4 6.7,10.7M7,18C6.4,18 6,17.6 6,17C6,16.4 6.4,16 7,16C7.6,16 8,16.4 8,17C8,17.6 7.6,18 7,18M19,6A2,2 0 0,0 17,4H15A2,2 0 0,0 13,2H11A2,2 0 0,0 9,4H7A2,2 0 0,0 5,6L4,18A2,2 0 0,0 6,20H8L7,22H17.1L16.1,20H18A2,2 0 0,0 20,18L19,6Z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"bus\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M4,16c0,0.88 0.39,1.67 1,2.22L5,20c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h8v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1.78c0.61,-0.55 1,-1.34 1,-2.22L20,6c0,-3.5 -3.58,-4 -8,-4s-8,0.5 -8,4v10zM7.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5S6.67,14 7.5,14s1.5,0.67 1.5,1.5S8.33,17 7.5,17zM16.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM18,11L6,11L6,6h12v5z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"walk\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM9.8 8.9L7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"bike\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t\t<path\n\t\t\t\t\td=\"M15.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM5 12c-2.8 0-5 2.2-5 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 8.5c-1.9 0-3.5-1.6-3.5-3.5s1.6-3.5 3.5-3.5 3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5zm5.8-10l2.4-2.4.8.8c1.3 1.3 3 2.1 5.1 2.1V9c-1.5 0-2.7-.6-3.6-1.5l-1.9-1.9c-.5-.4-1-.6-1.6-.6s-1.1.2-1.4.6L7.8 8.4c-.4.4-.6.9-.6 1.4 0 .6.2 1.1.6 1.4L11 14v5h2v-6.2l-2.2-2.3zM19 12c-2.8 0-5 2.2-5 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 8.5c-1.9 0-3.5-1.6-3.5-3.5s1.6-3.5 3.5-3.5 3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"cargo_bike\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\td=\"M21 11.5V10l-7.5-1V5H9v1.5h3v7.8h-1L9 10c.3-.1.5-.4.5-.7c0-.4-.3-.8-.7-.8h-2c-.5 0-.8.3-.8.7s.3.8.8.8h.6l2 4.2H7.9C7.6 12.4 6 11 4 11c-2.2 0-4 1.8-4 4s1.8 4 4 4c2 0 3.6-1.4 3.9-3.2h8.6c.2-2.4 2.1-4.3 4.5-4.3M6.4 15.8c-.3 1-1.3 1.8-2.4 1.8c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5c1.1 0 2.1.7 2.4 1.8H4v1.5h2.4M21 13c-1.7 0-3 1.3-3 3s1.3 3 3 3s3-1.3 3-3s-1.3-3-3-3m0 4.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5s1.5.7 1.5 1.5s-.7 1.5-1.5 1.5\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"scooter\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\td=\"M7.82 19H15v-1c0-2.21 1.79-4 4-4h.74l-1.9-8.44A2.01 2.01 0 0 0 15.89 4H12v2h3.89l1.4 6.25h-.01A6.01 6.01 0 0 0 13.09 17H7.82a2.996 2.996 0 0 0-3.42-1.94c-1.18.23-2.13 1.2-2.35 2.38A3.002 3.002 0 0 0 5 21c1.3 0 2.4-.84 2.82-2M5 19c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1m14-4c-1.66 0-3 1.34-3 3s1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3m0 4c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol\n\t\t\t\tid=\"seated_scooter\"\n\t\t\t\tstyle=\"\n\t\t\t\t\tfill-rule: evenodd;\n\t\t\t\t\tclip-rule: evenodd;\n\t\t\t\t\tstroke-linecap: round;\n\t\t\t\t\tstroke-linejoin: round;\n\t\t\t\t\tstroke-miterlimit: 1.5;\n\t\t\t\t\"\n\t\t\t\tviewBox=\"0 0 21 17\"\n\t\t\t>\n\t\t\t\t<path\n\t\t\t\t\td=\"M7.82 19H15v-1c0-2.21 1.79-4 4-4h.74l-1.9-8.44A2.015 2.015 0 0 0 15.89 4H12v2h3.89l1.4 6.25h-.01A6.02 6.02 0 0 0 13.09 17H7.82a3.006 3.006 0 0 0-3.42-1.94c-1.18.23-2.13 1.2-2.35 2.38a3 3 0 0 0-.052.558A3.016 3.016 0 0 0 5 21c1.3 0 2.4-.84 2.82-2M5 19c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m14-4c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3m0 4c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1\"\n\t\t\t\t\tstyle=\"fill-rule: nonzero\"\n\t\t\t\t\ttransform=\"translate(-1.998 -4)\"\n\t\t\t\t/>\n\t\t\t\t<path\n\t\t\t\t\td=\"M481.596 365.179v-6.596h-1.556m1.556 0h1.572\"\n\t\t\t\t\tstyle=\"fill: none; stroke: currentColor; stroke-width: 2px\"\n\t\t\t\t\ttransform=\"translate(-474.824 -352.179)\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"moped\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\td=\"M19 15c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1m0-2c-1.66 0-3 1.34-3 3s1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3m-9-7H5v2h5zm7-1h-3v2h3v2.65L13.5 14H10V9H6c-2.21 0-4 1.79-4 4v3h2c0 1.66 1.34 3 3 3s3-1.34 3-3h4.5l4.5-5.65V7a2 2 0 0 0-2-2M7 17c-.55 0-1-.45-1-1h2c0 .55-.45 1-1 1\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"car\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z\"\n\t\t\t\t/>\n\t\t\t\t<path d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t</symbol>\n\t\t\t<symbol id=\"ship\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t\t<path\n\t\t\t\t\td=\"M20 21c-1.39 0-2.78-.47-4-1.32-2.44 1.71-5.56 1.71-8 0C6.78 20.53 5.39 21 4 21H2v2h2c1.38 0 2.74-.35 4-.99 2.52 1.29 5.48 1.29 8 0 1.26.65 2.62.99 4 .99h2v-2h-2zM3.95 19H4c1.6 0 3.02-.88 4-2 .98 1.12 2.4 2 4 2s3.02-.88 4-2c.98 1.12 2.4 2 4 2h.05l1.89-6.68c.08-.26.06-.54-.06-.78s-.34-.42-.6-.5L20 10.62V6c0-1.1-.9-2-2-2h-3V1H9v3H6c-1.1 0-2 .9-2 2v4.62l-1.29.42c-.26.08-.48.26-.6.5s-.15.52-.06.78L3.95 19zM6 6h12v3.97L12 8 6 9.97V6z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol id=\"plane\" viewBox=\"0 0 24 24\">\n\t\t\t\t<path d=\"M0 0h24v24H0z\" fill=\"none\" />\n\t\t\t\t<path\n\t\t\t\t\td=\"M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol\n\t\t\t\tid=\"taxi\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstroke-width=\"2\"\n\t\t\t\tstroke-linecap=\"round\"\n\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path d=\"M10 2h4\" />\n\t\t\t\t<path d=\"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8\" />\n\t\t\t\t<path d=\"M7 14h.01\" />\n\t\t\t\t<path d=\"M17 14h.01\" />\n\t\t\t\t<rect width=\"18\" height=\"8\" x=\"3\" y=\"10\" rx=\"2\" />\n\t\t\t\t<path d=\"M5 18v2\" />\n\t\t\t\t<path d=\"M19 18v2\" />\n\t\t\t</symbol>\n\t\t\t<symbol\n\t\t\t\tid=\"funicular\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\tstroke-width=\"2\"\n\t\t\t\tstroke-linecap=\"round\"\n\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path\n\t\t\t\t\td=\"M2.225 22.675q-.45.125-.837-.162T1 21.725q0-.325.188-.575t.512-.35L6 19.625V17H4q-.425 0-.712-.288T3 16V6q-.425 0-.712-.288T2 5t.288-.712T3 4h2V3q0-.425.288-.712T6 2h12q.425 0 .713.288T19 3v1h2q.425 0 .713.288T22 5t-.288.713T21 6v8q0 .425-.288.713T20 15h-2v1.375l3.775-1.05q.45-.125.838.163t.387.787q0 .325-.187.575t-.513.35zM9 18.825l6-1.65V15h-2v1q0 .425-.288.713T12 17H9zM5 15h6v-1H5zm8-2h6v-1h-6zm-8-1h6V6H5zm8-2h6V6h-6zm-2 5v-1zm2-2v-1z\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol\n\t\t\t\tid=\"aerial_lift\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstroke-width=\"2\"\n\t\t\t\tstroke-linecap=\"round\"\n\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path\n\t\t\t\t\td=\"M4.413 11.624a17.8 17.8 0 0 0-.215 6.462c.246 1.64.37 2.461 1.213 3.188C6.255 22 7.277 22 9.322 22h5.356c2.045 0 3.067 0 3.91-.726c.845-.727.968-1.547 1.215-3.188a17.8 17.8 0 0 0-.216-6.462c-.337-1.533-.506-2.3-1.329-2.962S16.468 8 14.533 8H9.468c-1.935 0-2.903 0-3.726.662s-.992 1.428-1.33 2.962M4 2h10m6 0h-6m0 0l-2 3.5m-3 0h6M4.5 16h15M12 9v7\"\n\t\t\t\t/>\n\t\t\t</symbol>\n\t\t\t<symbol\n\t\t\t\tid=\"other\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstroke-width=\"2\"\n\t\t\t\tstroke-linecap=\"round\"\n\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t>\n\t\t\t\t<circle cx=\"12\" cy=\"12\" r=\"10\" />\n\t\t\t\t<path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\" />\n\t\t\t\t<path d=\"M12 17h.01\" />\n\t\t\t</symbol>\n\t\t</svg>\n\t</body>\n</html>\n"
  },
  {
    "path": "ui/src/index.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\n\ndescribe('sum test', () => {\n\tit('adds 1 + 2 to equal 3', () => {\n\t\texpect(1 + 2).toBe(3);\n\t});\n});\n"
  },
  {
    "path": "ui/src/lib/AddressTypeahead.svelte",
    "content": "<script lang=\"ts\">\n\timport { Combobox } from 'bits-ui';\n\timport { geocode, type LocationType, type Match, type Mode } from '@motis-project/motis-client';\n\timport { MapPinHouse as House, MapPin as Place } from '@lucide/svelte';\n\timport { parseCoordinatesToLocation, type Location } from './Location';\n\timport { language } from './i18n/translation';\n\timport maplibregl from 'maplibre-gl';\n\timport { getModeStyle, type LegLike } from './modeStyle';\n\n\tlet {\n\t\titems = $bindable([]),\n\t\tselected = $bindable(),\n\t\tplaceholder,\n\t\tname,\n\t\tplace,\n\t\ttype,\n\t\ttransitModes,\n\t\tonChange = () => {}\n\t}: {\n\t\titems?: Array<Location>;\n\t\tselected: Location;\n\t\tplaceholder?: string;\n\t\tname?: string;\n\t\tplace?: maplibregl.LngLatLike;\n\t\ttype?: undefined | LocationType;\n\t\ttransitModes?: Mode[];\n\t\tonChange?: (location: Location) => void;\n\t} = $props();\n\n\tlet inputValue = $state('');\n\tlet match = $state('');\n\n\tconst getDisplayArea = (match: Match | undefined) => {\n\t\tif (match) {\n\t\t\tconst matchedArea = match.areas.find((a) => a.matched);\n\t\t\tconst defaultArea = match.areas.find((a) => a.default);\n\t\t\tif (matchedArea?.name.match(/^[0-9]*$/)) {\n\t\t\t\tmatchedArea.name += ' ' + defaultArea?.name;\n\t\t\t}\n\n\t\t\t/* eslint-disable-next-line svelte/prefer-svelte-reactivity */\n\t\t\tconst areas = new Set<number>();\n\n\t\t\tmatch.areas.forEach((a, i) => {\n\t\t\t\tif (a.matched || a.unique || a.default || a.adminLevel == 2 || a.adminLevel == 4) {\n\t\t\t\t\tif (a.name != match.name) {\n\t\t\t\t\t\tareas.add(i);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tconst sorted = Array.from(areas);\n\t\t\tsorted.sort((a, b) => b - a);\n\n\t\t\treturn sorted.map((a) => match.areas[a].name).join(', ');\n\t\t}\n\t\treturn '';\n\t};\n\n\tconst getLabel = (match: Match) => {\n\t\tconst displayArea = getDisplayArea(match);\n\t\treturn displayArea ? match.name + ', ' + displayArea : match.name;\n\t};\n\n\tconst updateGuesses = async () => {\n\t\tconst coord = parseCoordinatesToLocation(inputValue);\n\t\tif (coord) {\n\t\t\tselected = coord;\n\t\t\titems = [];\n\t\t\tonChange(selected);\n\t\t\treturn;\n\t\t}\n\n\t\tconst pos = place ? maplibregl.LngLat.convert(place) : undefined;\n\t\tconst biasPlace = pos ? { place: `${pos.lat},${pos.lng}` } : {};\n\t\tconst { data: matches, error } = await geocode({\n\t\t\tquery: {\n\t\t\t\t...biasPlace,\n\t\t\t\ttext: inputValue,\n\t\t\t\tlanguage: [language],\n\t\t\t\tmode: transitModes,\n\t\t\t\ttype\n\t\t\t}\n\t\t});\n\t\tif (error) {\n\t\t\tconsole.error('TYPEAHEAD ERROR: ', error);\n\t\t\treturn;\n\t\t}\n\t\titems = matches!.map((match: Match): Location => {\n\t\t\treturn {\n\t\t\t\tlabel: getLabel(match),\n\t\t\t\tmatch\n\t\t\t};\n\t\t});\n\t};\n\n\tconst deserialize = (s: string): Location => {\n\t\tconst x = JSON.parse(s);\n\t\treturn {\n\t\t\tmatch: x,\n\t\t\tlabel: getLabel(x)\n\t\t};\n\t};\n\n\t$effect(() => {\n\t\tif (selected) {\n\t\t\tmatch = JSON.stringify(selected.match);\n\t\t\tinputValue = selected.label!;\n\t\t}\n\t});\n\n\tlet ref = $state<HTMLElement | null>(null);\n\t$effect(() => {\n\t\tif (ref && inputValue) {\n\t\t\t(ref as HTMLInputElement).value = inputValue;\n\t\t}\n\t});\n\n\tlet timer: number;\n\t$effect(() => {\n\t\tif (inputValue) {\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tupdateGuesses();\n\t\t\t}, 150);\n\t\t}\n\t});\n</script>\n\n{#snippet modeCircle(mode: Mode)}\n\t{@const modeIcon = getModeStyle({ mode } as LegLike)[0]}\n\t{@const modeColor = getModeStyle({ mode } as LegLike)[1]}\n\t<div\n\t\tstyle=\"background-color: {modeColor}; fill: white;\"\n\t\tclass=\"rounded-full flex items-center justify-center p-1\"\n\t>\n\t\t<svg class=\"relative size-4 rounded-full\">\n\t\t\t<use xlink:href={`#${modeIcon}`}></use>\n\t\t</svg>\n\t</div>\n{/snippet}\n\n<Combobox.Root\n\ttype=\"single\"\n\tallowDeselect={false}\n\tvalue={match}\n\tonValueChange={(e: string) => {\n\t\tif (e) {\n\t\t\tselected = deserialize(e);\n\t\t\tinputValue = selected.label!;\n\t\t\tonChange(selected);\n\t\t}\n\t}}\n>\n\t<Combobox.Input\n\t\t{placeholder}\n\t\t{name}\n\t\tbind:ref\n\t\tclass=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\"\n\t\tautocomplete=\"off\"\n\t\toninput={(e: Event) => (inputValue = (e.currentTarget as HTMLInputElement).value)}\n\t\taria-label={placeholder}\n\t\tdata-combobox-input={inputValue}\n\t/>\n\t{#if items.length !== 0}\n\t\t<Combobox.Portal>\n\t\t\t<Combobox.Content\n\t\t\t\talign=\"start\"\n\t\t\t\tclass=\"absolute top-2 w-[var(--bits-combobox-anchor-width)] z-10 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none\"\n\t\t\t>\n\t\t\t\t{#each items as item (item.match)}\n\t\t\t\t\t<Combobox.Item\n\t\t\t\t\t\tclass=\"flex w-full cursor-default select-none rounded-sm py-4 pl-4 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50\"\n\t\t\t\t\t\tvalue={JSON.stringify(item.match)}\n\t\t\t\t\t\tlabel={item.label}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div class=\"flex items-center grow\">\n\t\t\t\t\t\t\t<div class=\"size-6\">\n\t\t\t\t\t\t\t\t{#if item.match?.type == 'STOP'}\n\t\t\t\t\t\t\t\t\t{@render modeCircle(item.match.modes?.length ? item.match.modes![0] : 'BUS')}\n\t\t\t\t\t\t\t\t{:else if item.match?.type == 'ADDRESS'}\n\t\t\t\t\t\t\t\t\t<House class=\"size-5\" />\n\t\t\t\t\t\t\t\t{:else if item.match?.type == 'PLACE'}\n\t\t\t\t\t\t\t\t\t{#if !item.match?.category || item.match?.category == 'none'}\n\t\t\t\t\t\t\t\t\t\t<Place class=\"size-5\" />\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\tsrc={`icons/categories/${item.match?.category}.svg`}\n\t\t\t\t\t\t\t\t\t\t\talt={item.match?.category}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"size-5\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"flex flex-col ml-4\">\n\t\t\t\t\t\t\t\t<span class=\"font-semibold text-nowrap text-ellipsis overflow-hidden\">\n\t\t\t\t\t\t\t\t\t{item.match?.name}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"text-muted-foreground text-nowrap text-ellipsis overflow-hidden\">\n\t\t\t\t\t\t\t\t\t{getDisplayArea(item.match)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{#if item.match?.type == 'STOP'}\n\t\t\t\t\t\t\t<div class=\"mt-1 ml-4 flex flex-row gap-1.5 items-center\">\n\t\t\t\t\t\t\t\t{#each item.match.modes! as mode, i (i)}\n\t\t\t\t\t\t\t\t\t{@render modeCircle(mode)}\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</Combobox.Item>\n\t\t\t\t{/each}\n\t\t\t</Combobox.Content>\n\t\t</Combobox.Portal>\n\t{/if}\n</Combobox.Root>\n"
  },
  {
    "path": "ui/src/lib/AdvancedOptions.svelte",
    "content": "<script lang=\"ts\">\n\timport type { Snippet } from 'svelte';\n\timport Button from '$lib/components/ui/button/button.svelte';\n\timport { t } from '$lib/i18n/translation';\n\timport * as Select from '$lib/components/ui/select';\n\timport { ChevronUp, ChevronDown } from '@lucide/svelte';\n\timport { Switch } from './components/ui/switch';\n\timport type { ElevationCosts, ServerConfig } from '@motis-project/motis-client';\n\timport { defaultQuery } from '$lib/defaults';\n\timport type { Location } from '$lib/Location';\n\timport { formatDurationSec } from './formatDuration';\n\timport {\n\t\tpossibleTransitModes,\n\t\tprePostDirectModes,\n\t\ttype PrePostDirectMode,\n\t\ttype TransitMode\n\t} from './Modes';\n\timport NumberSelect from '$lib/NumberSelect.svelte';\n\timport StreetModes from '$lib/StreetModes.svelte';\n\timport TransitModeSelect from '$lib/TransitModeSelect.svelte';\n\timport { type NumberSelectOption } from '$lib/NumberSelect.svelte';\n\timport { generateTimes } from './generateTimes';\n\timport ViaStopOptions from './ViaStopOptions.svelte';\n\n\tlet {\n\t\tuseRoutedTransfers = $bindable(),\n\t\tserverConfig,\n\t\twheelchair = $bindable(),\n\t\trequireBikeTransport = $bindable(),\n\t\trequireCarTransport = $bindable(),\n\t\ttransitModes = $bindable(),\n\t\tmaxTransfers = $bindable(),\n\t\tmaxTravelTime = $bindable(undefined),\n\t\tpossibleMaxTravelTimes = [],\n\t\tpreTransitModes = $bindable(),\n\t\tpostTransitModes = $bindable(),\n\t\tdirectModes = $bindable(undefined),\n\t\tmaxPreTransitTime = $bindable(),\n\t\tmaxPostTransitTime = $bindable(),\n\t\tmaxDirectTime = $bindable(undefined),\n\t\televationCosts = $bindable(),\n\t\tignorePreTransitRentalReturnConstraints = $bindable(),\n\t\tignorePostTransitRentalReturnConstraints = $bindable(),\n\t\tignoreDirectRentalReturnConstraints = $bindable(),\n\t\tpreTransitProviderGroups = $bindable(),\n\t\tpostTransitProviderGroups = $bindable(),\n\t\tdirectProviderGroups = $bindable(),\n\t\tvia = $bindable(),\n\t\tviaMinimumStay = $bindable(),\n\t\tviaLabels = $bindable(),\n\t\thasDebug = false,\n\t\tadditionalComponents\n\t}: {\n\t\tuseRoutedTransfers: boolean;\n\t\tserverConfig: ServerConfig | undefined;\n\t\twheelchair: boolean;\n\t\trequireBikeTransport: boolean;\n\t\trequireCarTransport: boolean;\n\t\ttransitModes: TransitMode[];\n\t\tmaxTransfers: number;\n\t\tmaxTravelTime: number | undefined;\n\t\tpossibleMaxTravelTimes?: NumberSelectOption[];\n\t\tpreTransitModes: PrePostDirectMode[];\n\t\tpostTransitModes: PrePostDirectMode[];\n\t\tdirectModes: PrePostDirectMode[] | undefined;\n\t\tmaxPreTransitTime: number;\n\t\tmaxPostTransitTime: number;\n\t\tmaxDirectTime: number | undefined;\n\t\televationCosts: ElevationCosts;\n\t\tignorePreTransitRentalReturnConstraints: boolean;\n\t\tignorePostTransitRentalReturnConstraints: boolean;\n\t\tignoreDirectRentalReturnConstraints: boolean | undefined;\n\t\tpreTransitProviderGroups: string[];\n\t\tpostTransitProviderGroups: string[];\n\t\tdirectProviderGroups: string[];\n\t\tvia: undefined | Location[];\n\t\tviaMinimumStay: undefined | number[];\n\t\tviaLabels: Record<string, string>;\n\t\thasDebug: boolean;\n\t\tadditionalComponents?: Snippet;\n\t} = $props();\n\n\tconst possibleMaxTransfers = [...Array(defaultQuery.maxTransfers + 1).keys()].map((i) => ({\n\t\tvalue: i.toString(),\n\t\tlabel: i.toString()\n\t}));\n\n\tconst possibleDirectDurations = $derived(\n\t\tgenerateTimes(serverConfig?.maxDirectTimeLimit ?? 60 * 60)\n\t);\n\tconst possiblePrePostDurations = $derived(\n\t\tgenerateTimes(serverConfig?.maxPrePostTransitTimeLimit ?? 60 * 60)\n\t);\n\n\tfunction setModes(mode: PrePostDirectMode) {\n\t\treturn function (checked: boolean) {\n\t\t\tif (checked) {\n\t\t\t\tpreTransitModes = [mode];\n\t\t\t\tpostTransitModes = [mode];\n\t\t\t\tdirectModes = [mode];\n\t\t\t}\n\t\t};\n\t}\n\tif (transitModes.includes('TRANSIT')) {\n\t\ttransitModes = [...possibleTransitModes];\n\t}\n\tif (requireBikeTransport) {\n\t\tsetModes('BIKE')(true);\n\t}\n\tif (requireCarTransport) {\n\t\tsetModes('CAR')(true);\n\t}\n\n\tconst possibleElevationCosts = [\n\t\t{ value: 'NONE' as ElevationCosts, label: t.elevationCosts.NONE },\n\t\t{ value: 'LOW' as ElevationCosts, label: t.elevationCosts.LOW },\n\t\t{ value: 'HIGH' as ElevationCosts, label: t.elevationCosts.HIGH }\n\t];\n\tlet expanded = $state<boolean>(false);\n\tlet allowElevationCosts = $derived(\n\t\tserverConfig?.hasElevation &&\n\t\t\t(requireBikeTransport ||\n\t\t\t\tpreTransitModes.includes('BIKE') ||\n\t\t\t\tpostTransitModes.includes('BIKE') ||\n\t\t\t\tdirectModes?.includes('BIKE'))\n\t);\n\tlet allowStreetRouting = $derived(serverConfig?.hasStreetRouting);\n\tlet allowRoutedTransfers = $derived(serverConfig?.hasRoutedTransfers);\n\n\tlet possibleModes = $derived(\n\t\thasDebug ? prePostDirectModes : prePostDirectModes.filter((m) => !m.startsWith('DEBUG_'))\n\t);\n</script>\n\n<Button variant=\"ghost\" onclick={() => (expanded = !expanded)}>\n\t{t.advancedSearchOptions}\n\t{#if expanded}\n\t\t<ChevronUp class=\"size-[18px]\" />\n\t{:else}\n\t\t<ChevronDown class=\"size-[18px]\" />\n\t{/if}\n</Button>\n\n{#if expanded}\n\t<div class=\"w-full space-y-4\">\n\t\t<TransitModeSelect bind:transitModes />\n\n\t\t<div class=\"space-y-2\">\n\t\t\t<Switch\n\t\t\t\tbind:checked={useRoutedTransfers}\n\t\t\t\tdisabled={!allowRoutedTransfers}\n\t\t\t\tlabel={t.useRoutedTransfers}\n\t\t\t\tid=\"useRoutedTransfers\"\n\t\t\t\tonCheckedChange={(checked) => {\n\t\t\t\t\tif (wheelchair && !checked) {\n\t\t\t\t\t\twheelchair = false;\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t/>\n\n\t\t\t<Switch\n\t\t\t\tbind:checked={wheelchair}\n\t\t\t\tdisabled={!allowRoutedTransfers}\n\t\t\t\tlabel={t.wheelchair}\n\t\t\t\tid=\"wheelchair\"\n\t\t\t\tonCheckedChange={(checked) => {\n\t\t\t\t\tif (checked && !useRoutedTransfers) {\n\t\t\t\t\t\tuseRoutedTransfers = true;\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<Switch\n\t\t\t\tbind:checked={requireBikeTransport}\n\t\t\t\tlabel={t.requireBikeTransport}\n\t\t\t\tonCheckedChange={setModes('BIKE')}\n\t\t\t\tid=\"requireBikeTransport\"\n\t\t\t/>\n\t\t\t<Switch\n\t\t\t\tbind:checked={requireCarTransport}\n\t\t\t\tdisabled={!allowRoutedTransfers}\n\t\t\t\tlabel={t.requireCarTransport}\n\t\t\t\tid=\"requireCarTransport\"\n\t\t\t\tonCheckedChange={(checked) => {\n\t\t\t\t\tif (checked && !useRoutedTransfers && allowRoutedTransfers) {\n\t\t\t\t\t\tuseRoutedTransfers = true;\n\t\t\t\t\t}\n\t\t\t\t\tsetModes('CAR')(checked);\n\t\t\t\t}}\n\t\t\t/>\n\n\t\t\t<ViaStopOptions bind:via bind:viaMinimumStay bind:viaLabels />\n\n\t\t\t<div\n\t\t\t\tclass=\"grid {maxTravelTime === undefined\n\t\t\t\t\t? 'grid-cols-2'\n\t\t\t\t\t: 'grid-cols-4'} items-center gap-2\"\n\t\t\t>\n\t\t\t\t<div class=\"text-sm\">\n\t\t\t\t\t{t.routingSegments.maxTransfers}\n\t\t\t\t</div>\n\t\t\t\t<NumberSelect bind:value={maxTransfers} possibleValues={possibleMaxTransfers} />\n\t\t\t\t{#if maxTravelTime !== undefined}\n\t\t\t\t\t<div class=\"text-sm\">\n\t\t\t\t\t\t{t.routingSegments.maxTravelTime}\n\t\t\t\t\t</div>\n\t\t\t\t\t<NumberSelect\n\t\t\t\t\t\tbind:value={maxTravelTime}\n\t\t\t\t\t\tpossibleValues={possibleMaxTravelTimes}\n\t\t\t\t\t\tlabelFormatter={formatDurationSec}\n\t\t\t\t\t/>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t<!-- First mile -->\n\t\t\t<StreetModes\n\t\t\t\tlabel={t.routingSegments.firstMile}\n\t\t\t\tdisabled={!allowStreetRouting}\n\t\t\t\tbind:modes={preTransitModes}\n\t\t\t\tbind:maxTransitTime={maxPreTransitTime}\n\t\t\t\t{possibleModes}\n\t\t\t\tpossibleMaxTransitTime={possiblePrePostDurations}\n\t\t\t\tbind:ignoreRentalReturnConstraints={ignorePreTransitRentalReturnConstraints}\n\t\t\t\tbind:providerGroups={preTransitProviderGroups}\n\t\t\t></StreetModes>\n\n\t\t\t<!-- Last mile -->\n\t\t\t<StreetModes\n\t\t\t\tlabel={t.routingSegments.lastMile}\n\t\t\t\tdisabled={!allowStreetRouting}\n\t\t\t\tbind:modes={postTransitModes}\n\t\t\t\tbind:maxTransitTime={maxPostTransitTime}\n\t\t\t\t{possibleModes}\n\t\t\t\tpossibleMaxTransitTime={possiblePrePostDurations}\n\t\t\t\tbind:ignoreRentalReturnConstraints={ignorePostTransitRentalReturnConstraints}\n\t\t\t\tbind:providerGroups={postTransitProviderGroups}\n\t\t\t></StreetModes>\n\n\t\t\t<!-- Direct -->\n\t\t\t{#if directModes !== undefined && maxDirectTime !== undefined && ignoreDirectRentalReturnConstraints !== undefined}\n\t\t\t\t<StreetModes\n\t\t\t\t\tlabel={t.routingSegments.direct}\n\t\t\t\t\tdisabled={!allowStreetRouting}\n\t\t\t\t\tbind:modes={directModes}\n\t\t\t\t\tbind:maxTransitTime={maxDirectTime}\n\t\t\t\t\t{possibleModes}\n\t\t\t\t\tpossibleMaxTransitTime={possibleDirectDurations}\n\t\t\t\t\tbind:ignoreRentalReturnConstraints={ignoreDirectRentalReturnConstraints}\n\t\t\t\t\tbind:providerGroups={directProviderGroups}\n\t\t\t\t></StreetModes>\n\t\t\t{/if}\n\t\t</div>\n\n\t\t<!-- Elevation Costs -->\n\t\t<div class=\"grid grid-cols-2 items-center\">\n\t\t\t<div class=\"text-sm\">\n\t\t\t\t{t.selectElevationCosts}\n\t\t\t</div>\n\t\t\t<Select.Root\n\t\t\t\tdisabled={!allowElevationCosts || !allowStreetRouting}\n\t\t\t\ttype=\"single\"\n\t\t\t\tbind:value={elevationCosts}\n\t\t\t>\n\t\t\t\t<Select.Trigger aria-label={t.selectElevationCosts}>\n\t\t\t\t\t{t.elevationCosts[elevationCosts]}\n\t\t\t\t</Select.Trigger>\n\t\t\t\t<Select.Content sideOffset={10}>\n\t\t\t\t\t{#each possibleElevationCosts as costs, i (i + costs.value)}\n\t\t\t\t\t\t<Select.Item value={costs.value} label={costs.label}>\n\t\t\t\t\t\t\t{costs.label}\n\t\t\t\t\t\t</Select.Item>\n\t\t\t\t\t{/each}\n\t\t\t\t</Select.Content>\n\t\t\t</Select.Root>\n\t\t</div>\n\t\t{#if additionalComponents}\n\t\t\t{@render additionalComponents()}\n\t\t{/if}\n\t</div>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/Alerts.svelte",
    "content": "<script lang=\"ts\">\n\timport { Info, ChevronRight } from '@lucide/svelte';\n\timport * as Dialog from '$lib/components/ui/dialog';\n\timport { buttonVariants } from './components/ui/button';\n\timport type { Alert } from '@motis-project/motis-client';\n\timport { formatDateTime, getTz } from './toDateTime';\n\timport { cn } from './utils';\n\timport { t } from './i18n/translation';\n\n\tconst {\n\t\talerts = [],\n\t\tvariant = 'icon',\n\t\ttz\n\t}: {\n\t\talerts?: Alert[];\n\t\tvariant?: 'icon' | 'full';\n\t\ttz: string | undefined;\n\t} = $props();\n</script>\n\n{#if alerts.length > 0}\n\t<Dialog.Root>\n\t\t<Dialog.Trigger class=\"max-w-full pr-4  {variant == 'full' ? 'pt-2' : 'ml-2'}\">\n\t\t\t{#if variant === 'full'}\n\t\t\t\t<div\n\t\t\t\t\tclass={cn(\n\t\t\t\t\t\tbuttonVariants({ variant: 'outline' }),\n\t\t\t\t\t\t'max-w-full flex items-center bg-blue-50 dark:bg-blue-950 shadow-none'\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<div class=\"flex flex-col gap-1 overflow-hidden\">\n\t\t\t\t\t\t<div class=\"font-bold flex gap-2 items-center text-blue-700 dark:text-blue-500\">\n\t\t\t\t\t\t\t<Info />\n\t\t\t\t\t\t\t{t.alerts.information}\n\t\t\t\t\t\t\t{#if alerts.length > 1}\n\t\t\t\t\t\t\t\t<span class=\"text-muted-foreground font-normal\">\n\t\t\t\t\t\t\t\t\t+{alerts.length - 1}\n\t\t\t\t\t\t\t\t\t{t.alerts.more}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<span class=\"font-normal text-muted-foreground overflow-hidden text-ellipsis w-full\">\n\t\t\t\t\t\t\t{alerts[0].descriptionText || alerts[0].headerText}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<ChevronRight class=\"size-4\" />\n\t\t\t\t</div>\n\t\t\t{:else}\n\t\t\t\t<Info />\n\t\t\t{/if}\n\t\t</Dialog.Trigger>\n\t\t<Dialog.Content>\n\t\t\t<Dialog.Header>\n\t\t\t\t<Dialog.Description class=\"space-y-4\">\n\t\t\t\t\t{#each alerts as alert, i (i)}\n\t\t\t\t\t\t<div class=\"last:mb-0 text-justify\">\n\t\t\t\t\t\t\t<h3 class=\"font-bold text-blue-700 dark:text-blue-500 mb-1 flex items-center gap-2\">\n\t\t\t\t\t\t\t\t<Info class=\"size-5\" />{alert.headerText}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t{#if alert.impactPeriod}\n\t\t\t\t\t\t\t\t{#each alert.impactPeriod as impactPeriod, j (j)}\n\t\t\t\t\t\t\t\t\t{@const start = new Date(impactPeriod.start ?? 0)}\n\t\t\t\t\t\t\t\t\t{@const end = new Date(impactPeriod.end ?? 0)}\n\t\t\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t\t\t<strong>{t.alerts.validFrom}:</strong>\n\t\t\t\t\t\t\t\t\t\t{formatDateTime(start, tz)}\n\t\t\t\t\t\t\t\t\t\t<strong>{t.alerts.until}</strong>\n\t\t\t\t\t\t\t\t\t\t{formatDateTime(end, tz)}\n\t\t\t\t\t\t\t\t\t\t<span class=\"text-xs font-normal\">{getTz(start, tz)}</span>\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if alert.causeDetail}\n\t\t\t\t\t\t\t\t<p>{alert.causeDetail}</p>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if alert.descriptionText}\n\t\t\t\t\t\t\t\t<p>{alert.descriptionText}</p>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/each}\n\t\t\t\t</Dialog.Description>\n\t\t\t</Dialog.Header>\n\t\t</Dialog.Content>\n\t</Dialog.Root>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/Color.ts",
    "content": "export type RGBA = [number, number, number, number];\nexport function hexToRgb(hex: string): RGBA {\n\tconst result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n\tif (!result) {\n\t\tthrow `${hex} is not a hex color #RRGGBB`;\n\t}\n\treturn [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16), 255];\n}\n\nexport function rgbToHex(rgba: RGBA): string {\n\treturn '#' + ((1 << 24) | (rgba[0] << 16) | (rgba[1] << 8) | rgba[2]).toString(16).slice(1);\n}\n\nexport const getDelayColor = (delay: number, realTime: boolean): RGBA => {\n\tdelay = delay / 60000;\n\tif (!realTime) {\n\t\treturn [100, 100, 100, 255];\n\t}\n\tif (delay <= -30) {\n\t\treturn [255, 0, 255, 255];\n\t} else if (delay <= -6) {\n\t\treturn [138, 82, 254, 255];\n\t} else if (delay <= 3) {\n\t\treturn [69, 194, 74, 255];\n\t} else if (delay <= 5) {\n\t\treturn [255, 237, 0, 255];\n\t} else if (delay <= 10) {\n\t\treturn [255, 102, 0, 255];\n\t} else if (delay <= 15) {\n\t\treturn [255, 48, 71, 255];\n\t}\n\treturn [163, 0, 10, 255];\n};\n"
  },
  {
    "path": "ui/src/lib/ConnectionDetail.svelte",
    "content": "<script lang=\"ts\">\n\timport { ArrowRight, ArrowUp, ArrowDown, DollarSign, CircleX } from '@lucide/svelte';\n\timport type {\n\t\tFareProduct,\n\t\tItinerary,\n\t\tLeg,\n\t\tMode,\n\t\tPlace,\n\t\tStepInstruction\n\t} from '@motis-project/motis-client';\n\timport Time from '$lib/Time.svelte';\n\timport { routeBorderColor, routeColor } from '$lib/modeStyle';\n\timport { formatDurationSec, formatDistanceMeters } from '$lib/formatDuration';\n\timport { Button } from '$lib/components/ui/button';\n\timport Route from '$lib/Route.svelte';\n\timport Alerts from '$lib/Alerts.svelte';\n\timport { getModeName } from '$lib/getModeName';\n\timport { language, t } from '$lib/i18n/translation';\n\timport { onClickStop, onClickTrip } from '$lib/utils';\n\timport { formatDate, formatTime } from './toDateTime';\n\timport { getModeLabel } from './map/getModeLabel';\n\tconst {\n\t\titinerary\n\t}: {\n\t\titinerary: Itinerary;\n\t} = $props();\n\n\tconst isRelevantLeg = (l: Leg) => l.duration !== 0 || l.displayName;\n\tconst lastLeg = $derived(itinerary.legs.findLast(isRelevantLeg));\n</script>\n\n{#snippet stopTimes(\n\ttimestamp: string,\n\tscheduledTimestamp: string,\n\tisRealtime: boolean,\n\tp: Place,\n\tmode: Mode,\n\tisStartOrEnd: number,\n\thidePlatform?: boolean\n)}\n\t{@const arriveBy = isStartOrEnd == 0 || isStartOrEnd == 1}\n\t{@const textColor = isStartOrEnd == 0 ? '' : 'font-semibold'}\n\t<div class=\"flex items-baseline justify-between w-full {textColor}\">\n\t\t<div class=\"flex justify-between\">\n\t\t\t<Time\n\t\t\t\tvariant=\"schedule\"\n\t\t\t\tclass=\"w-14 md:w-16\"\n\t\t\t\tqueriedTime={timestamp}\n\t\t\t\ttimeZone={p.tz}\n\t\t\t\t{isRealtime}\n\t\t\t\t{timestamp}\n\t\t\t\t{scheduledTimestamp}\n\t\t\t\t{arriveBy}\n\t\t\t/>\n\t\t\t<Time\n\t\t\t\tvariant=\"realtime\"\n\t\t\t\tclass=\"w-14 md:w-16\"\n\t\t\t\ttimeZone={p.tz}\n\t\t\t\t{isRealtime}\n\t\t\t\t{timestamp}\n\t\t\t\t{scheduledTimestamp}\n\t\t\t\t{arriveBy}\n\t\t\t/>\n\t\t</div>\n\t\t<div class=\"w-full\">\n\t\t\t{#if p.stopId}\n\t\t\t\t{@const pickupNotAllowedOrEnd = p.pickupType == 'NOT_ALLOWED' && isStartOrEnd != 1}\n\t\t\t\t{@const dropoffNotAllowedOrStart = p.dropoffType == 'NOT_ALLOWED' && isStartOrEnd != -1}\n\t\t\t\t<div class=\"flex items-center justify-between\">\n\t\t\t\t\t<div class=\"flex flex-row items-center justify-center\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tclass=\"text-[length:inherit] leading-none justify-normal text-wrap p-0 text-left {textColor}\"\n\t\t\t\t\t\t\tvariant=\"link\"\n\t\t\t\t\t\t\tonclick={() => onClickStop(p.name, p.stopId!, new Date(timestamp))}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{p.name}\n\t\t\t\t\t\t</Button>\n\n\t\t\t\t\t\t{#if isStartOrEnd != 0}\n\t\t\t\t\t\t\t<Alerts alerts={p.alerts} tz={p.tz} variant=\"icon\" />\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\t\t\t\t\t{#if p.track && !hidePlatform}\n\t\t\t\t\t\t<span class=\"text-nowrap px-2 border rounded-xl ml-1 mr-4\">\n\t\t\t\t\t\t\t{getModeLabel(mode) == 'Track' ? t.trackAbr : t.platformAbr}\n\t\t\t\t\t\t\t{p.track}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t{#if (p as Place & { switchTo?: Leg }).switchTo}\n\t\t\t\t\t\t{@const switchTo = (p as Place & { switchTo: Leg }).switchTo}\n\t\t\t\t\t\t<div class=\"flex items-center text-sm mt-1\">\n\t\t\t\t\t\t\t{t.continuesAs}\n\t\t\t\t\t\t\t{switchTo.displayName!}\n\t\t\t\t\t\t\t<ArrowRight class=\"mx-1 size-4\" />\n\t\t\t\t\t\t\t{switchTo.headsign}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if pickupNotAllowedOrEnd || dropoffNotAllowedOrStart}\n\t\t\t\t\t\t<div class=\"flex items-center text-destructive text-sm mt-1\">\n\t\t\t\t\t\t\t<CircleX class=\"stroke-destructive size-4\" />\n\t\t\t\t\t\t\t<span class=\"ml-1 leading-none\">\n\t\t\t\t\t\t\t\t{pickupNotAllowedOrEnd && dropoffNotAllowedOrStart\n\t\t\t\t\t\t\t\t\t? t.inOutDisallowed\n\t\t\t\t\t\t\t\t\t: pickupNotAllowedOrEnd\n\t\t\t\t\t\t\t\t\t\t? t.inDisallowed\n\t\t\t\t\t\t\t\t\t\t: t.outDisallowed}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t{:else}\n\t\t\t\t<span>{p.name || p.flex}</span>\n\t\t\t{/if}\n\t\t</div>\n\t</div>\n{/snippet}\n\n{#snippet streetLeg(l: Leg)}\n\t{@const stepsWithElevation = l.steps?.filter(\n\t\t(s: StepInstruction) => s.elevationUp || s.elevationDown\n\t)}\n\t{@const stepsWithToll = l.steps?.filter((s: StepInstruction) => s.toll)}\n\t{@const stepsWithAccessRestriction = l.steps?.filter((s: StepInstruction) => s.accessRestriction)}\n\n\t<div class=\"py-12 flex flex-col gap-y-4 text-muted-foreground\">\n\t\t<span class=\"ml-6\">\n\t\t\t{formatDurationSec(l.duration)}\n\t\t\t{getModeName(l)}\n\t\t\t{formatDistanceMeters(l.distance)}\n\t\t</span>\n\t\t{#if l.rental && l.rental.systemName}\n\t\t\t<span class=\"ml-6\">\n\t\t\t\t{t.sharingProvider}:\n\t\t\t\t<a href={l.rental.url} target=\"_blank\" class=\"hover:underline\">{l.rental.systemName}</a>\n\t\t\t</span>\n\t\t{/if}\n\t\t{#if l.rental?.returnConstraint == 'ROUNDTRIP_STATION'}\n\t\t\t<span class=\"ml-6\">\n\t\t\t\t{t.roundtripStationReturnConstraint}\n\t\t\t</span>\n\t\t{/if}\n\t\t{#if l.rental?.rentalUriWeb}\n\t\t\t<span class=\"ml-6\">\n\t\t\t\t<Button class=\"font-bold\" variant=\"outline\" href={l.rental.rentalUriWeb} target=\"_blank\">\n\t\t\t\t\t{t.rent}\n\t\t\t\t</Button>\n\t\t\t</span>\n\t\t{/if}\n\t\t{#if stepsWithElevation && stepsWithElevation.length > 0}\n\t\t\t<div class=\"ml-6 flex items-center gap-2 text-xs\">\n\t\t\t\t{t.incline}\n\t\t\t\t<div class=\"flex items-center\">\n\t\t\t\t\t<ArrowUp class=\"size-4\" />\n\t\t\t\t\t{stepsWithElevation.reduce((acc: number, s: StepInstruction) => acc + s.elevationUp!, 0)} m\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex items-center\">\n\t\t\t\t\t<ArrowDown class=\"size-4\" />\n\t\t\t\t\t{stepsWithElevation.reduce(\n\t\t\t\t\t\t(acc: number, s: StepInstruction) => acc + s.elevationDown!,\n\t\t\t\t\t\t0\n\t\t\t\t\t)} m\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{/if}\n\t\t{#if stepsWithToll && stepsWithToll.length > 0}\n\t\t\t<div class=\"ml-6 flex items-center gap-2 text-sm text-orange-500\">\n\t\t\t\t<DollarSign class=\"size-4\" />\n\t\t\t\t{t.toll}\n\t\t\t</div>\n\t\t{/if}\n\t\t{#if stepsWithAccessRestriction && stepsWithAccessRestriction.length > 0}\n\t\t\t<div class=\"ml-6 flex items-center gap-2 text-sm text-orange-500\">\n\t\t\t\t<CircleX class=\"size-4\" />\n\t\t\t\t{t.accessRestriction}\n\t\t\t\t({stepsWithAccessRestriction\n\t\t\t\t\t.map((s) => s.accessRestriction)\n\t\t\t\t\t.filter((value, index, array) => array.indexOf(value) === index)\n\t\t\t\t\t.join(', ')})\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n{/snippet}\n\n{#snippet productInfo(product: FareProduct)}\n\t{product.name}\n\t{new Intl.NumberFormat(language, { style: 'currency', currency: product.currency }).format(\n\t\tproduct.amount\n\t)}\n\t{#if product.riderCategory}\n\t\tfor\n\t\t{#if product.riderCategory.eligibilityUrl}\n\t\t\t<a\n\t\t\t\tclass:italic={product.riderCategory.isDefaultFareCategory}\n\t\t\t\tclass=\"underline\"\n\t\t\t\thref={product.riderCategory.eligibilityUrl}\n\t\t\t>\n\t\t\t\t{product.riderCategory.riderCategoryName}\n\t\t\t</a>\n\t\t{:else}\n\t\t\t<span class:italic={product.riderCategory.isDefaultFareCategory}>\n\t\t\t\t{product.riderCategory.riderCategoryName}\n\t\t\t</span>\n\t\t{/if}\n\t{/if}\n\t{#if product.media}\n\t\tas\n\t\t{#if product.media.fareMediaName}\n\t\t\t{product.media.fareMediaName}\n\t\t{:else}\n\t\t\t{product.media.fareMediaType}\n\t\t{/if}\n\t{/if}\n{/snippet}\n\n{#snippet ticketInfo(prevTransitLeg: Leg | undefined, l: Leg)}\n\t{#if itinerary.fareTransfers != undefined && l.fareTransferIndex != undefined && l.effectiveFareLegIndex != undefined}\n\t\t{@const fareTransfer = itinerary.fareTransfers[l.fareTransferIndex]}\n\t\t{@const includedInTransfer =\n\t\t\tfareTransfer.rule == 'AB' || (fareTransfer.rule == 'A_AB' && l.effectiveFareLegIndex !== 0)}\n\t\t{#if includedInTransfer || fareTransfer.effectiveFareLegProducts[l.effectiveFareLegIndex].length > 0}\n\t\t\t<div class=\"pl-1 md:pl-4 my-8 text-xs font-bold\">\n\t\t\t\t{#if includedInTransfer || (prevTransitLeg && prevTransitLeg.fareTransferIndex === l.fareTransferIndex && prevTransitLeg.effectiveFareLegIndex === l.effectiveFareLegIndex)}\n\t\t\t\t\t{t.includedInTicket}\n\t\t\t\t{:else}\n\t\t\t\t\t{@const productOptions = fareTransfer.effectiveFareLegProducts[l.effectiveFareLegIndex]}\n\t\t\t\t\t{#if productOptions.length > 1}\n\t\t\t\t\t\t<div class=\"mb-1\">{t.ticketOptions}:</div>\n\t\t\t\t\t{/if}\n\t\t\t\t\t<ul\n\t\t\t\t\t\tclass:list-disc={productOptions.length > 1}\n\t\t\t\t\t\tclass:list-outside={productOptions.length > 1}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#each productOptions as products, i (i)}\n\t\t\t\t\t\t\t{#each products as product, j (j)}\n\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\t\t{@render productInfo(product)}\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</ul>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t{/if}\n\t{/if}\n{/snippet}\n\n<div class=\"text-lg max-w-full\">\n\t{#each itinerary.legs as l, i (i)}\n\t\t{@const isLast = i == itinerary.legs.length - 1}\n\t\t{@const isLastPred = i == itinerary.legs.length - 2}\n\t\t{@const pred = i == 0 ? undefined : itinerary.legs[i - 1]}\n\t\t{@const next = isLast ? undefined : itinerary.legs[i + 1]}\n\t\t{@const prevTransitLeg = itinerary.legs.slice(0, i).find((l) => l.tripId)}\n\t\t{#if l.displayName}\n\t\t\t<div class=\"w-full flex justify-between items-center space-x-1\">\n\t\t\t\t<Route {onClickTrip} {l} />\n\t\t\t\t{#if pred && (pred.from.track || isRelevantLeg(pred)) && (i != 1 || pred.displayName)}\n\t\t\t\t\t<div class=\"border-t h-0 grow shrink\"></div>\n\t\t\t\t\t<div class=\"text-sm text-muted-foreground leading-none px-2 text-center\">\n\t\t\t\t\t\t{#if pred.duration}\n\t\t\t\t\t\t\t{formatDurationSec(pred.duration)} {t.walk}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{#if pred.distance}\n\t\t\t\t\t\t\t({Math.round(pred.distance)} m)\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{#if prevTransitLeg?.fareTransferIndex != undefined && itinerary.fareTransfers && itinerary.fareTransfers[prevTransitLeg.fareTransferIndex].transferProducts}\n\t\t\t\t\t\t\t{@const transferProducts =\n\t\t\t\t\t\t\t\titinerary.fareTransfers[prevTransitLeg.fareTransferIndex].transferProducts!}\n\t\t\t\t\t\t\t{#if prevTransitLeg.effectiveFareLegIndex === 0 && l.effectiveFareLegIndex === 1}\n\t\t\t\t\t\t\t\t<br />\n\t\t\t\t\t\t\t\t<span class=\"text-xs font-bold text-foreground text-left\">\n\t\t\t\t\t\t\t\t\t{#if transferProducts.length > 1}\n\t\t\t\t\t\t\t\t\t\t<div class=\"mb-1\">{t.ticketOptions}:</div>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t<ul\n\t\t\t\t\t\t\t\t\t\tclass:list-disc={transferProducts.length > 1}\n\t\t\t\t\t\t\t\t\t\tclass:list-outside={transferProducts.length > 1}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{#each transferProducts as product, j (j)}\n\t\t\t\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\t\t\t\t\t{@render productInfo(product)}\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t\t<div class=\"border-t h-0 grow shrink\"></div>\n\t\t\t</div>\n\n\t\t\t<div class=\"pt-4 pb-2 pl-4 sm:pl-6 border-l-4 left-4 relative\" style={routeBorderColor(l)}>\n\t\t\t\t{@render stopTimes(l.startTime, l.scheduledStartTime, l.realTime, l.from, l.mode, -1)}\n\t\t\t\t<div class=\"flex items-center\">\n\t\t\t\t\t<ArrowRight class=\"stroke-muted-foreground size-4\" />\n\t\t\t\t\t<span class=\"ml-1\">\n\t\t\t\t\t\t{#if l.tripTo}\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tclass=\"text-[length:inherit] text-muted-foreground leading-none justify-normal text-wrap text-left \"\n\t\t\t\t\t\t\t\tvariant=\"link\"\n\t\t\t\t\t\t\t\tonclick={() =>\n\t\t\t\t\t\t\t\t\tonClickStop(\n\t\t\t\t\t\t\t\t\t\tl.tripTo!.name,\n\t\t\t\t\t\t\t\t\t\tl.tripTo!.stopId!,\n\t\t\t\t\t\t\t\t\t\tnew Date(l.tripTo!.arrival!),\n\t\t\t\t\t\t\t\t\t\ttrue\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{l.headsign}\n\t\t\t\t\t\t\t\t{#if !l.headsign || !l.tripTo.name.startsWith(l.headsign)}\n\t\t\t\t\t\t\t\t\t<br />({l.tripTo.name})\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t{l.headsign}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\n\t\t\t\t<Alerts alerts={l.alerts} tz={l.from.tz || l.to.tz} variant=\"full\" />\n\n\t\t\t\t{#if l.routeUrl}\n\t\t\t\t\t<div class=\"mt-2 mr-4\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\thref={l.routeUrl}\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\tclass=\"overflow-hidden text-ellipsis whitespace-nowrap w-full px-4 inline-block underline\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{l.routeUrl}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\n\t\t\t\t{#if l.loopedCalendarSince}\n\t\t\t\t\t<div class=\"mt-2 flex items-center text-destructive leading-none\">\n\t\t\t\t\t\t{t.dataExpiredSince}\n\t\t\t\t\t\t{formatDate(new Date(l.loopedCalendarSince), l.from.tz)}\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t\t{#if l.cancelled}\n\t\t\t\t\t<div class=\"mt-2 flex items-center text-destructive leading-none\">\n\t\t\t\t\t\t<CircleX class=\"stroke-destructive size-4\" />\n\t\t\t\t\t\t<span class=\"ml-1 font-bold\">{t.tripCancelled}</span>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t\t{#if !l.scheduled}\n\t\t\t\t\t<div class=\"mt-2 flex items-center text-green-600 leading-none\">\n\t\t\t\t\t\t<span class=\"ml-1\">{t.unscheduledTrip}</span>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t\t{#if l.intermediateStops?.length === 0}\n\t\t\t\t\t<div class=\"py-10 pl-4 md:pl-4 flex items-center text-muted-foreground\">\n\t\t\t\t\t\t{t.tripIntermediateStops(0)} ({formatDurationSec(l.duration)})\n\t\t\t\t\t</div>\n\t\t\t\t\t{@render ticketInfo(prevTransitLeg, l)}\n\t\t\t\t{:else}\n\t\t\t\t\t{@render ticketInfo(prevTransitLeg, l)}\n\t\t\t\t\t<details class=\"[&_.collapsible]:open:-rotate-180 my-2\">\n\t\t\t\t\t\t<summary class=\"py-10 pl-4 md:pl-4 flex items-center text-muted-foreground\">\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\tclass=\"collapsible rotate-0 transform transition-all duration-300\"\n\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\theight=\"20\"\n\t\t\t\t\t\t\t\twidth=\"20\"\n\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\tstroke-linecap=\"round\"\n\t\t\t\t\t\t\t\tstroke-linejoin=\"round\"\n\t\t\t\t\t\t\t\tstroke-width=\"2\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<polyline points=\"6 9 12 15 18 9\"></polyline>\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t<span class=\"ml-2 cursor-pointer\">\n\t\t\t\t\t\t\t\t{t.tripIntermediateStops(l.intermediateStops?.length ?? 0)}\n\t\t\t\t\t\t\t\t({formatDurationSec(l.duration)})\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</summary>\n\t\t\t\t\t\t<div class=\"grid gap-2 items-start content-start pb-2\">\n\t\t\t\t\t\t\t{#each l.intermediateStops! as s, i (i)}\n\t\t\t\t\t\t\t\t{@render stopTimes(s.arrival!, s.scheduledArrival!, l.realTime, s, l.mode, 0)}\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</details>\n\t\t\t\t{/if}\n\n\t\t\t\t{#if !isLast && !(isLastPred && !isRelevantLeg(next!))}\n\t\t\t\t\t{@render stopTimes(l.endTime!, l.scheduledEndTime!, l.realTime!, l.to, l.mode, 1)}\n\t\t\t\t{/if}\n\n\t\t\t\t{#if isLast || (isLastPred && !isRelevantLeg(next!))}\n\t\t\t\t\t<!-- fill visual gap -->\n\t\t\t\t\t<div class=\"pb-2\"></div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t{: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')))}\n\t\t\t<Route {onClickTrip} {l} />\n\t\t\t<div class=\"pt-2 pb-2 pl-4 sm:pl-6 border-l-4 left-4 relative\" style={routeBorderColor(l)}>\n\t\t\t\t{@render stopTimes(l.startTime, l.scheduledStartTime, l.realTime, l.from, l.mode, -1, true)}\n\t\t\t\t{#if l.mode == 'FLEX'}\n\t\t\t\t\t<div class=\"mt-2 flex items-center leading-none\">\n\t\t\t\t\t\t<span class=\"ml-1 text-sm\">\n\t\t\t\t\t\t\t{formatTime(new Date(l.from.flexStartPickupDropOffWindow!), l.from.tz)} -\n\t\t\t\t\t\t\t{formatTime(new Date(l.from.flexEndPickupDropOffWindow!), l.from.tz)}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t\t{@render streetLeg(l)}\n\t\t\t\t{#if !isLast}\n\t\t\t\t\t{@render stopTimes(l.endTime, l.scheduledEndTime, l.realTime, l.to, l.mode, 1, true)}\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t{/if}\n\t{/each}\n\t<div class=\"relative pl-4 md:pl-6 left-5\">\n\t\t<div\n\t\t\tclass=\"absolute left-[-9px] w-[15px] h-[15px] rounded-full\"\n\t\t\tstyle={routeColor(lastLeg!)}\n\t\t></div>\n\t\t<div class=\"relative top-[-6px] mb-[-6px]\">\n\t\t\t{@render stopTimes(\n\t\t\t\tlastLeg!.endTime,\n\t\t\t\tlastLeg!.scheduledEndTime,\n\t\t\t\tlastLeg!.realTime,\n\t\t\t\tlastLeg!.to,\n\t\t\t\tlastLeg!.mode,\n\t\t\t\t1\n\t\t\t)}\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "ui/src/lib/DateInput.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn } from './utils';\n\n\tlet {\n\t\tvalue = $bindable(),\n\t\tclass: className\n\t}: {\n\t\tvalue: Date;\n\t\tclass?: string;\n\t} = $props();\n\n\tlet el: undefined | HTMLInputElement;\n\n\t$effect(() => {\n\t\tif (el !== undefined) {\n\t\t\tvalue.setSeconds(0, 0);\n\t\t\tconst dateTimeLocalValue = new Date(value.getTime() - value.getTimezoneOffset() * 60000)\n\t\t\t\t.toISOString()\n\t\t\t\t.slice(0, -1);\n\t\t\tel.value = dateTimeLocalValue;\n\t\t}\n\t});\n</script>\n\n<input\n\ttype=\"datetime-local\"\n\tclass={cn(\n\t\t'flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',\n\t\tclassName\n\t)}\n\tbind:this={el}\n\tonchange={(e) => {\n\t\t// @ts-expect-error target exists, value exists\n\t\tconst dateTimeLocalValue = e.target!.value!;\n\t\tconst fakeUtcTime = new Date(`${dateTimeLocalValue}Z`);\n\t\tif (!isNaN(fakeUtcTime.getTime())) {\n\t\t\t/* eslint-disable-next-line svelte/prefer-svelte-reactivity */\n\t\t\tvalue = new Date(fakeUtcTime.getTime() + fakeUtcTime.getTimezoneOffset() * 60000);\n\t\t}\n\t}}\n/>\n"
  },
  {
    "path": "ui/src/lib/Debug.svelte",
    "content": "<script lang=\"ts\">\n\timport { Bug, X, LoaderCircle } from '@lucide/svelte';\n\timport { Button } from '$lib/components/ui/button';\n\timport GeoJSON from '$lib/map/GeoJSON.svelte';\n\timport Layer from '$lib/map/Layer.svelte';\n\timport {\n\t\tTable,\n\t\tTableBody,\n\t\tTableCell,\n\t\tTableHead,\n\t\tTableHeader,\n\t\tTableRow\n\t} from '$lib/components/ui/table';\n\timport maplibregl from 'maplibre-gl';\n\timport { transfers } from '@motis-project/motis-client';\n\timport Control from '$lib/map/Control.svelte';\n\timport * as Card from '$lib/components/ui/card';\n\timport Marker from '$lib/map/Marker.svelte';\n\timport { posToLocation, type Location as ApiLocation } from '$lib/Location';\n\timport geojson from 'geojson';\n\timport Popup from '$lib/map/Popup.svelte';\n\timport { client } from '@motis-project/motis-client';\n\timport DateInput from './DateInput.svelte';\n\n\tconst baseUrl = client.getConfig().baseUrl;\n\n\tconst post = async (path: string, req: unknown) => {\n\t\tconst response = await fetch(`${baseUrl}${path}`, {\n\t\t\tmethod: 'POST',\n\t\t\tmode: 'cors',\n\t\t\theaders: {\n\t\t\t\t'Access-Control-Allow-Origin': '*',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(req)\n\t\t});\n\t\treturn await response.json();\n\t};\n\n\tconst get = async (path: string) => {\n\t\tconst response = await fetch(`${baseUrl}${path}`, {\n\t\t\tmethod: 'GET',\n\t\t\tmode: 'cors',\n\t\t\theaders: {\n\t\t\t\t'Access-Control-Allow-Origin': '*',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t}\n\t\t});\n\t\treturn await response.json();\n\t};\n\n\ttype Location = {\n\t\tlat: number;\n\t\tlng: number;\n\t\tlevel: number;\n\t};\n\n\ttype RoutingQuery = {\n\t\tstart: Location;\n\t\tdestination: Location;\n\t\tprofile: string;\n\t\tdirection: string;\n\t};\n\n\ttype ElevatorStatus = 'ACTIVE' | 'INACTIVE';\n\n\ttype Elevator = {\n\t\tid: number;\n\t\tstatus: ElevatorStatus;\n\t\tdesc: string;\n\t\toutOfService: [Date, Date][];\n\t};\n\n\tconst toLocation = (l: ApiLocation): Location => {\n\t\treturn {\n\t\t\tlat: l.match!.lat,\n\t\t\tlng: l.match!.lon,\n\t\t\tlevel: l.match!.level ?? 0\n\t\t};\n\t};\n\n\tconst getRoute = async (query: RoutingQuery) => {\n\t\treturn await post('/api/route', query);\n\t};\n\n\tconst getMatches = async (bounds: maplibregl.LngLatBounds) => {\n\t\treturn await post('/api/matches', bounds.toArray().flat());\n\t};\n\n\tconst getFlex = async (bounds: maplibregl.LngLatBounds) => {\n\t\tconst min = bounds.getSouthWest();\n\t\tconst max = bounds.getNorthEast();\n\t\treturn await get(`/api/debug/flex?min=${min.lat},${min.lng}&max=${max.lat},${max.lng}`);\n\t};\n\n\tconst getElevators = async (bounds: maplibregl.LngLatBounds) => {\n\t\treturn await post('/api/elevators', bounds.toArray().flat());\n\t};\n\n\tconst updateElevator = async (e: { id: number; status: ElevatorStatus }) => {\n\t\tconsole.log(JSON.stringify(e));\n\t\treturn await post('/api/update_elevator', e);\n\t};\n\n\texport const getGraph = async (bounds: maplibregl.LngLatBounds, level: number) => {\n\t\treturn await post('/api/graph', {\n\t\t\tlevel: level,\n\t\t\twaypoints: bounds.toArray().flat()\n\t\t});\n\t};\n\n\tlet {\n\t\tbounds,\n\t\tlevel,\n\t\tzoom\n\t}: {\n\t\tbounds: maplibregl.LngLatBoundsLike | undefined;\n\t\tlevel: number;\n\t\tzoom: number;\n\t} = $props();\n\n\tlet debug = $state(false);\n\tlet id = $state<string>();\n\tlet fps = $derived(\n\t\tid && bounds && debug ? transfers<false>({ query: { id } }).then((x) => x.data) : undefined\n\t);\n\tlet matches = $derived(\n\t\tbounds && debug && zoom > 15 ? getMatches(maplibregl.LngLatBounds.convert(bounds)) : undefined\n\t);\n\tlet flex = $derived(\n\t\tbounds && debug ? getFlex(maplibregl.LngLatBounds.convert(bounds)) : undefined\n\t);\n\n\tconst parseElevator = (e: { outOfService: string }) => {\n\t\treturn {\n\t\t\t...e,\n\t\t\toutOfService: JSON.parse(e.outOfService).map(([from, to]: [string, string]) => {\n\t\t\t\treturn [new Date(from), new Date(to)];\n\t\t\t})\n\t\t};\n\t};\n\n\tlet graph = $state<null | geojson.GeoJSON>(null);\n\tlet elevators = $state<null | geojson.GeoJSON>(null);\n\t$effect(() => {\n\t\tif (debug && bounds && zoom > 15) {\n\t\t\tgetGraph(maplibregl.LngLatBounds.convert(bounds), level).then((response: geojson.GeoJSON) => {\n\t\t\t\tgraph = response;\n\t\t\t});\n\t\t\tgetElevators(maplibregl.LngLatBounds.convert(bounds)).then((response: geojson.GeoJSON) => {\n\t\t\t\televators = response;\n\t\t\t});\n\t\t} else {\n\t\t\tgraph = null;\n\t\t}\n\t});\n\n\tlet profile = $state<string>();\n\tlet start = $state.raw<ApiLocation>();\n\tlet destination = $state.raw<ApiLocation>();\n\tlet route = $derived(\n\t\tstart?.match?.lat &&\n\t\t\tstart?.match.lon &&\n\t\t\tdestination?.match?.lat &&\n\t\t\tdestination?.match.lon &&\n\t\t\tprofile\n\t\t\t? getRoute({\n\t\t\t\t\tstart: toLocation(start),\n\t\t\t\t\tdestination: toLocation(destination),\n\t\t\t\t\tprofile,\n\t\t\t\t\tdirection: 'forward'\n\t\t\t\t})\n\t\t\t: undefined\n\t);\n\n\tlet elevatorUpdate = $state<Promise<Response> | null>(null);\n\tlet elevator = $state<Elevator | null>(null);\n</script>\n\n<Button\n\tsize=\"icon\"\n\tvariant={debug ? 'default' : 'outline'}\n\tonclick={() => {\n\t\tdebug = !debug;\n\t}}\n>\n\t<Bug size=\"icon\" class=\"h-[1.2rem] w-[1.2rem]\" />\n</Button>\n\n<!-- eslint-disable-next-line -->\n{#snippet propertiesTable(_1: maplibregl.MapMouseEvent, _2: () => void, features: any)}\n\t<Table>\n\t\t<TableBody>\n\t\t\t{#each Object.entries(features[0].properties) as [key, value], i (i)}\n\t\t\t\t<TableRow>\n\t\t\t\t\t<TableCell class=\"px-2 py-0\">{key}</TableCell>\n\t\t\t\t\t<TableCell class=\"px-2 py-0\">\n\t\t\t\t\t\t{#if key === 'osm_node_id'}\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://www.openstreetmap.org/node/{value}\"\n\t\t\t\t\t\t\t\tclass=\"underline bold text-blue-400\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t{:else if key === 'osm_way_id'}\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://www.openstreetmap.org/way/{value}\"\n\t\t\t\t\t\t\t\tclass=\"underline bold text-blue-400\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</TableCell>\n\t\t\t\t</TableRow>\n\t\t\t{/each}\n\t\t</TableBody>\n\t</Table>\n{/snippet}\n\n{#if elevator}\n\t<Control position=\"bottom-right\">\n\t\t<Card.Root class=\"min-w-[550px] mb-4\">\n\t\t\t<div class=\"w-full flex justify-between bg-muted items-center\">\n\t\t\t\t<h2 class=\"text-lg ml-2 font-bold\">\n\t\t\t\t\tFahrstuhl {elevator.desc}\n\t\t\t\t\t<span class=\"ml-2 text-sm text-muted-foreground\">\n\t\t\t\t\t\t{elevator.id}\n\t\t\t\t\t</span>\n\t\t\t\t</h2>\n\t\t\t\t<Button variant=\"ghost\" onclick={() => (elevator = null)}>\n\t\t\t\t\t<X />\n\t\t\t\t</Button>\n\t\t\t</div>\n\n\t\t\t<Card.Content class=\"flex flex-col gap-6\">\n\t\t\t\t<Table>\n\t\t\t\t\t<TableHeader>\n\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t<TableHead class=\"font-semibold\">Not available from</TableHead>\n\t\t\t\t\t\t\t<TableHead class=\"font-semibold\">to</TableHead>\n\t\t\t\t\t\t\t<TableHead></TableHead>\n\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t</TableHeader>\n\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t{#if elevator.outOfService}\n\t\t\t\t\t\t\t{#each elevator.outOfService as _, i (i)}\n\t\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t<DateInput bind:value={elevator.outOfService[i][0]} />\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t<DateInput bind:value={elevator.outOfService[i][1]} />\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" onclick={() => elevator!.outOfService!.splice(i, 1)}>\n\t\t\t\t\t\t\t\t\t\t\t<X />\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</TableBody>\n\t\t\t\t</Table>\n\n\t\t\t\t<div class=\"flex justify-between gap-4\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tonclick={() => elevator!.outOfService.push([new Date(), new Date()])}\n\t\t\t\t\t>\n\t\t\t\t\t\tAdd Maintainance\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tclass=\"w-48\"\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\televatorUpdate = updateElevator(elevator!);\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if elevatorUpdate != null}\n\t\t\t\t\t\t\t{#await elevatorUpdate}\n\t\t\t\t\t\t\t\t<LoaderCircle class=\"animate-spin w-4 h-4\" />\n\t\t\t\t\t\t\t{:then _}\n\t\t\t\t\t\t\t\tUpdate\n\t\t\t\t\t\t\t{/await}\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\tUpdate\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</Button>\n\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\televatorUpdate = updateElevator({\n\t\t\t\t\t\t\t\tid: elevator!.id,\n\t\t\t\t\t\t\t\tstatus: elevator!.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if elevator.status === 'ACTIVE'}\n\t\t\t\t\t\t\tDEACTIVATE\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\tACTIVATE\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</Card.Content>\n\t\t</Card.Root>\n\t</Control>\n{/if}\n\n{#if debug}\n\t{#if fps}\n\t\t{#await fps then f}\n\t\t\t{#if f}\n\t\t\t\t<Control position=\"bottom-right\">\n\t\t\t\t\t<Card.Root class=\"w-[600px] h-[500px] overflow-y-auto bg-background rounded-lg\">\n\t\t\t\t\t\t<div class=\"w-full flex justify-between items-center shadow-md pl-1 mb-1\">\n\t\t\t\t\t\t\t<h2 class=\"ml-2 text-base font-semibold\">\n\t\t\t\t\t\t\t\t{f.place.name}\n\t\t\t\t\t\t\t\t{f.place.track}\n\t\t\t\t\t\t\t\t<span class=\"text-xs text-muted-foreground font-mono\">\n\t\t\t\t\t\t\t\t\t{f.place.stopId}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"text-sm text-muted-foreground\">Level: {f.place.level}</span>\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\tid = undefined;\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<X />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Table>\n\t\t\t\t\t\t\t<TableHeader>\n\t\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Station</TableHead>\n\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Default</TableHead>\n\t\t\t\t\t\t\t\t\t{#if f.hasFootTransfers}\n\t\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Foot</TableHead>\n\t\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Foot Routed</TableHead>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t{#if f.hasWheelchairTransfers}\n\t\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Wheelchair</TableHead>\n\t\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Wheelchair Routed</TableHead>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t{#if f.hasCarTransfers}\n\t\t\t\t\t\t\t\t\t\t<TableHead class=\"text-center\">Car</TableHead>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t</TableHeader>\n\t\t\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t\t\t{#each f.transfers as x, i (i)}\n\t\t\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t{x.to.name} <br />\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"text-xs text-muted-foreground font-mono\">\n\t\t\t\t\t\t\t\t\t\t\t\t{x.to.stopId}\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t{#if x.default !== undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstart = posToLocation(f.place, f.place.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdestination = posToLocation(x.to, x.to.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tprofile = 'foot';\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{x.default}\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t{#if f.hasFootTransfers}\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t{#if x.foot !== undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstart = posToLocation(f.place, f.place.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdestination = posToLocation(x.to, x.to.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprofile = 'foot';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{x.foot}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t{#if x.footRouted !== undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstart = posToLocation(f.place, f.place.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdestination = posToLocation(x.to, x.to.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprofile = 'foot';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{x.footRouted}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t{#if f.hasWheelchairTransfers}\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t{#if x.wheelchair !== undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass={x.wheelchairUsesElevator ? 'text-red-500' : 'text-green-500'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstart = posToLocation(f.place, f.place.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdestination = posToLocation(x.to, x.to.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprofile = 'wheelchair';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{x.wheelchair}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t{#if x.wheelchairRouted !== undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass={x.wheelchairUsesElevator ? 'text-red-500' : 'text-green-500'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstart = posToLocation(f.place, f.place.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdestination = posToLocation(x.to, x.to.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprofile = 'wheelchair';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{x.wheelchairRouted}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t{#if f.hasCarTransfers}\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t{#if x.car !== undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstart = posToLocation(f.place, f.place.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdestination = posToLocation(x.to, x.to.level);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprofile = 'car';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{x.car}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</TableBody>\n\t\t\t\t\t\t</Table>\n\t\t\t\t\t</Card.Root>\n\t\t\t\t</Control>\n\t\t\t{/if}\n\t\t{/await}\n\t{/if}\n\n\t{#if matches}\n\t\t{#await matches then m}\n\t\t\t<GeoJSON id=\"matches\" data={m}>\n\t\t\t\t<Layer\n\t\t\t\t\tonclick={(e) => {\n\t\t\t\t\t\tconst props = e.features![0].properties;\n\t\t\t\t\t\tid = props.id;\n\t\t\t\t\t}}\n\t\t\t\t\tid=\"matches\"\n\t\t\t\t\ttype=\"circle\"\n\t\t\t\t\tfilter={['all', ['==', '$type', 'Point']]}\n\t\t\t\t\tlayout={{}}\n\t\t\t\t\tpaint={{\n\t\t\t\t\t\t'circle-color': ['match', ['get', 'type'], 'location', '#34ebde', '#fa921b'],\n\t\t\t\t\t\t'circle-radius': 5\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t\t</Layer>\n\t\t\t\t<Layer\n\t\t\t\t\tid=\"match\"\n\t\t\t\t\ttype=\"line\"\n\t\t\t\t\tfilter={['all', ['==', 'type', 'match']]}\n\t\t\t\t\tlayout={{\n\t\t\t\t\t\t'line-join': 'round',\n\t\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t\t}}\n\t\t\t\t\tpaint={{\n\t\t\t\t\t\t'line-color': '#00ff00',\n\t\t\t\t\t\t'line-width': 3\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t\t</Layer>\n\t\t\t</GeoJSON>\n\t\t{/await}\n\t{/if}\n\n\t{#if flex}\n\t\t{#await flex then f}\n\t\t\t<GeoJSON id=\"flex\" data={f}>\n\t\t\t\t<Layer\n\t\t\t\t\tonclick={(e) => {\n\t\t\t\t\t\tconst props = e.features![0].properties;\n\t\t\t\t\t\tid = props.id;\n\t\t\t\t\t}}\n\t\t\t\t\tid=\"flex-location-groups\"\n\t\t\t\t\ttype=\"circle\"\n\t\t\t\t\tfilter={['all', ['==', '$type', 'Point']]}\n\t\t\t\t\tlayout={{}}\n\t\t\t\t\tpaint={{\n\t\t\t\t\t\t'circle-color': '#00ff00',\n\t\t\t\t\t\t'circle-radius': 5\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t\t</Layer>\n\t\t\t\t<Layer\n\t\t\t\t\tid=\"flex-areas\"\n\t\t\t\t\ttype=\"fill\"\n\t\t\t\t\tlayout={{}}\n\t\t\t\t\tfilter={['literal', true]}\n\t\t\t\t\tpaint={{\n\t\t\t\t\t\t'fill-color': '#088',\n\t\t\t\t\t\t'fill-opacity': 0.4,\n\t\t\t\t\t\t'fill-outline-color': '#000'\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t\t<Layer\n\t\t\t\t\tid=\"flex-areas-outline\"\n\t\t\t\t\ttype=\"line\"\n\t\t\t\t\tlayout={{}}\n\t\t\t\t\tfilter={['literal', true]}\n\t\t\t\t\tpaint={{\n\t\t\t\t\t\t'line-color': '#000',\n\t\t\t\t\t\t'line-width': 2\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t\t<Layer\n\t\t\t\t\tid=\"flex-areas-labels\"\n\t\t\t\t\ttype=\"symbol\"\n\t\t\t\t\tlayout={{\n\t\t\t\t\t\t'symbol-placement': 'point',\n\t\t\t\t\t\t'text-field': ['get', 'name'],\n\t\t\t\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t\t\t\t'text-size': 16\n\t\t\t\t\t}}\n\t\t\t\t\tfilter={['literal', true]}\n\t\t\t\t\tpaint={{\n\t\t\t\t\t\t'text-halo-width': 12,\n\t\t\t\t\t\t'text-halo-color': '#fff',\n\t\t\t\t\t\t'text-color': '#f00'\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</GeoJSON>\n\t\t{/await}\n\t{/if}\n\n\t{#if route}\n\t\t{#await route then r}\n\t\t\t{#if r.type == 'FeatureCollection'}\n\t\t\t\t<GeoJSON id=\"route\" data={r}>\n\t\t\t\t\t<Layer\n\t\t\t\t\t\tid=\"path-outline\"\n\t\t\t\t\t\ttype=\"line\"\n\t\t\t\t\t\tlayout={{\n\t\t\t\t\t\t\t'line-join': 'round',\n\t\t\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tfilter={['any', ['!has', 'level'], ['==', 'level', level]]}\n\t\t\t\t\t\tpaint={{\n\t\t\t\t\t\t\t'line-color': '#cfa900',\n\t\t\t\t\t\t\t'line-width': 7.5,\n\t\t\t\t\t\t\t'line-opacity': 0.8\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t<Layer\n\t\t\t\t\t\tid=\"path\"\n\t\t\t\t\t\ttype=\"line\"\n\t\t\t\t\t\tlayout={{\n\t\t\t\t\t\t\t'line-join': 'round',\n\t\t\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tfilter={['any', ['!has', 'level'], ['==', 'level', level]]}\n\t\t\t\t\t\tpaint={{\n\t\t\t\t\t\t\t'line-color': '#fccf03',\n\t\t\t\t\t\t\t'line-width': 5,\n\t\t\t\t\t\t\t'line-opacity': 0.8\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</GeoJSON>\n\t\t\t{/if}\n\t\t{/await}\n\t{/if}\n\n\t{#if graph != null}\n\t\t<GeoJSON id=\"graph\" data={graph}>\n\t\t\t<Layer\n\t\t\t\tid=\"graph-geometry\"\n\t\t\t\ttype=\"line\"\n\t\t\t\tfilter={[\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'type', 'geometry'],\n\t\t\t\t\t['any', ['!has', 'level'], ['==', 'level', level]]\n\t\t\t\t]}\n\t\t\t\tlayout={{\n\t\t\t\t\t'line-join': 'round',\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t}}\n\t\t\t\tpaint={{\n\t\t\t\t\t'line-color': '#e55e5e',\n\t\t\t\t\t'line-width': 3,\n\t\t\t\t\t'line-opacity': 1\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t</Layer>\n\t\t\t<Layer\n\t\t\t\tid=\"graph-edge\"\n\t\t\t\ttype=\"line\"\n\t\t\t\tfilter={['all', ['==', 'type', 'edge'], ['any', ['!has', 'level'], ['==', 'level', level]]]}\n\t\t\t\tlayout={{\n\t\t\t\t\t'line-join': 'round',\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t}}\n\t\t\t\tpaint={{\n\t\t\t\t\t'line-color': '#a300d9',\n\t\t\t\t\t'line-width': 3\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t</Layer>\n\t\t\t<Layer\n\t\t\t\tid=\"graph-node\"\n\t\t\t\ttype=\"circle\"\n\t\t\t\tfilter={['all', ['==', '$type', 'Point']]}\n\t\t\t\tlayout={{}}\n\t\t\t\tpaint={{\n\t\t\t\t\t'circle-color': ['match', ['get', 'label'], 'unreachable', '#ff1150', '#11ffaf'],\n\t\t\t\t\t'circle-radius': 5\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t</Layer>\n\t\t</GeoJSON>\n\t{/if}\n\n\t{#if elevators}\n\t\t<GeoJSON id=\"elevators\" data={elevators}>\n\t\t\t<Layer\n\t\t\t\tid=\"elevators\"\n\t\t\t\ttype=\"circle\"\n\t\t\t\tfilter={['all', ['==', '$type', 'Point']]}\n\t\t\t\tlayout={{}}\n\t\t\t\tpaint={{\n\t\t\t\t\t'circle-color': ['match', ['get', 'status'], 'ACTIVE', '#ffff00', '#ff00ff'],\n\t\t\t\t\t'circle-radius': 8\n\t\t\t\t}}\n\t\t\t\tonclick={(e) => {\n\t\t\t\t\t// @ts-expect-error type mismatch\n\t\t\t\t\televator = parseElevator(e.features![0].properties);\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<Layer\n\t\t\t\tid=\"elevators-match\"\n\t\t\t\ttype=\"line\"\n\t\t\t\tfilter={['all', ['==', 'type', 'match']]}\n\t\t\t\tlayout={{\n\t\t\t\t\t'line-join': 'round',\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t}}\n\t\t\t\tpaint={{\n\t\t\t\t\t'line-color': '#00ff00',\n\t\t\t\t\t'line-width': 3\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Popup trigger=\"click\" children={propertiesTable} />\n\t\t\t</Layer>\n\t\t</GeoJSON>\n\t{/if}\n\n\t{#if start}\n\t\t<Marker color=\"magenta\" draggable={false} location={start} />\n\t{/if}\n\n\t{#if destination}\n\t\t<Marker color=\"yellow\" draggable={false} location={destination} />\n\t{/if}\n{/if}\n"
  },
  {
    "path": "ui/src/lib/DeparturesMask.svelte",
    "content": "<script lang=\"ts\">\n\timport AddressTypeahead from '$lib/AddressTypeahead.svelte';\n\timport { type Location } from '$lib/Location';\n\timport { t } from '$lib/i18n/translation';\n\timport { onClickStop } from '$lib/utils';\n\n\tlet {\n\t\ttime = $bindable()\n\t}: {\n\t\ttime: Date;\n\t} = $props();\n\n\tlet from = $state<Location>() as Location;\n\tlet fromItems = $state<Array<Location>>([]);\n</script>\n\n<div id=\"searchmask-container\" class=\"flex flex-col space-y-4 p-4 relative\">\n\t<AddressTypeahead\n\t\tname=\"from\"\n\t\tplaceholder={t.from}\n\t\tbind:selected={from}\n\t\tbind:items={fromItems}\n\t\ttype=\"STOP\"\n\t\tonChange={(location) => {\n\t\t\tif (location.match) {\n\t\t\t\tonClickStop(location.label, location.match.id, time);\n\t\t\t}\n\t\t}}\n\t/>\n</div>\n"
  },
  {
    "path": "ui/src/lib/DirectConnection.svelte",
    "content": "<script lang=\"ts\">\n\timport { Button, type ButtonProps } from '$lib/components/ui/button';\n\timport { formatDurationSec } from '$lib/formatDuration';\n\timport { getModeStyle, routeColor } from './modeStyle';\n\timport type { Itinerary } from '@motis-project/motis-client';\n\n\tconst {\n\t\td,\n\t\t...restProps\n\t}: {\n\t\td: Itinerary;\n\t} & ButtonProps = $props();\n\n\tconst modeStyles = [\n\t\t...new Map(d.legs.map((l) => [JSON.stringify(getModeStyle(l)), getModeStyle(l)])).values()\n\t];\n\n\tconst leg = d.legs.find((leg) => leg.mode !== 'WALK') ?? d.legs[0]!;\n</script>\n\n<Button variant=\"child\" {...restProps}>\n\t<div\n\t\tclass=\"flex items-center py-1 px-2 rounded-lg font-bold text-sm h-8 text-nowrap\"\n\t\tstyle={routeColor(leg)}\n\t>\n\t\t{#each modeStyles as [icon, _color, _textColor], i (i)}\n\t\t\t<svg class=\"relative mr-1 w-4 h-4 rounded-full\">\n\t\t\t\t<use xlink:href={`#${icon}`}></use>\n\t\t\t</svg>\n\t\t{/each}\n\t\t{formatDurationSec(d.duration)}\n\t</div>\n</Button>\n"
  },
  {
    "path": "ui/src/lib/ErrorMessage.svelte",
    "content": "<script lang=\"ts\">\n\timport { CircleAlert, CircleCheck, SearchX, ServerCrash } from '@lucide/svelte';\n\n\tlet {\n\t\tmessage,\n\t\tstatus\n\t}: {\n\t\tmessage: string;\n\t\tstatus: number | undefined;\n\t} = $props();\n\n\tconst getErrorType = (status: number) => {\n\t\tswitch (status) {\n\t\t\tcase 200:\n\t\t\t\treturn 'OK';\n\t\t\tcase 400:\n\t\t\t\treturn 'Bad Request';\n\t\t\tcase 404:\n\t\t\t\treturn 'Not Found';\n\t\t\tcase 500:\n\t\t\t\treturn 'Internal Server Error';\n\t\t\tcase 422:\n\t\t\t\treturn 'Unprocessable Entity';\n\t\t\tdefault:\n\t\t\t\treturn 'Unknown';\n\t\t}\n\t};\n\n\tconst getErrorIcon = (status: number) => {\n\t\tswitch (status) {\n\t\t\tcase 200:\n\t\t\t\treturn CircleCheck;\n\t\t\tcase 400:\n\t\t\t\treturn CircleAlert;\n\t\t\tcase 404:\n\t\t\t\treturn SearchX;\n\t\t\tcase 422:\n\t\t\tcase 500:\n\t\t\t\treturn ServerCrash;\n\t\t\tdefault:\n\t\t\t\treturn SearchX;\n\t\t}\n\t};\n\tlet Icon = $state(getErrorIcon(status ?? 404));\n</script>\n\n<div\n\tclass=\"p-4 my-4 mx-auto min-w-96 max-w-fit flex flex-col items-center gap-5 rounded-lg border border-destructive/20\"\n>\n\t<div class=\"flex items-center gap-4 mx-auto\">\n\t\t<Icon class=\"h-7 w-7 text-destructive\" />\n\t\t<h2 class=\"text-xl font-semibold text-destructive\">\n\t\t\t{status}\n\t\t\t{getErrorType(status ?? 404)}\n\t\t</h2>\n\t</div>\n\t<p class=\"text-lg text-muted-foreground max-w-[40ch] break-words\">\n\t\t{message}\n\t</p>\n</div>\n"
  },
  {
    "path": "ui/src/lib/IsochronesInfo.svelte",
    "content": "<script lang=\"ts\">\n\timport { t } from '$lib/i18n/translation';\n\timport { LoaderCircle } from '@lucide/svelte';\n\timport ErrorMessage from '$lib/ErrorMessage.svelte';\n\timport type { IsochronesOptions } from '$lib/map/IsochronesShared';\n\n\tlet {\n\t\toptions\n\t}: {\n\t\toptions: IsochronesOptions;\n\t} = $props();\n</script>\n\n<div>\n\t{#if options.status == 'WORKING'}\n\t\t<div class=\"flex items-center justify-center w-full\">\n\t\t\t<LoaderCircle class=\"animate-spin w-12 h-12 m-4\" />\n\t\t</div>\n\t{/if}\n\t{#if options.status == 'EMPTY'}\n\t\t<ErrorMessage message={t.isochrones.noData} status={404} />\n\t{/if}\n\t{#if options.status == 'FAILED'}\n\t\t<ErrorMessage message={options.errorMessage!} status={options.errorCode} />\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/IsochronesMask.svelte",
    "content": "<script lang=\"ts\">\n\timport { t } from '$lib/i18n/translation';\n\timport { Slider } from 'bits-ui';\n\timport { LocateFixed } from '@lucide/svelte';\n\timport maplibregl from 'maplibre-gl';\n\timport * as RadioGroup from '$lib/components/ui/radio-group';\n\timport Button from '$lib/components/ui/button/button.svelte';\n\timport { Label } from '$lib/components/ui/label';\n\timport {\n\t\ttype ElevationCosts,\n\t\ttype PedestrianProfile,\n\t\ttype ServerConfig\n\t} from '@motis-project/motis-client';\n\timport * as Select from '$lib/components/ui/select';\n\timport type { DisplayLevel, IsochronesOptions } from '$lib/map/IsochronesShared';\n\timport AddressTypeahead from '$lib/AddressTypeahead.svelte';\n\timport AdvancedOptions from '$lib/AdvancedOptions.svelte';\n\timport DateInput from '$lib/DateInput.svelte';\n\timport { posToLocation, type Location } from '$lib/Location';\n\timport { formatDurationSec } from '$lib/formatDuration';\n\timport type { PrePostDirectMode, TransitMode } from '$lib/Modes';\n\timport { generateTimes } from './generateTimes';\n\n\tlet {\n\t\tone = $bindable(),\n\t\tmaxTravelTime = $bindable(),\n\t\tserverConfig,\n\t\tgeocodingBiasPlace,\n\t\ttime = $bindable(),\n\t\tuseRoutedTransfers = $bindable(),\n\t\tpedestrianProfile = $bindable(),\n\t\trequireBikeTransport = $bindable(),\n\t\trequireCarTransport = $bindable(),\n\t\ttransitModes = $bindable(),\n\t\tmaxTransfers = $bindable(),\n\t\tpreTransitModes = $bindable(),\n\t\tpostTransitModes = $bindable(),\n\t\tmaxPreTransitTime = $bindable(),\n\t\tmaxPostTransitTime = $bindable(),\n\t\tarriveBy = $bindable(),\n\t\televationCosts = $bindable(),\n\t\tignorePreTransitRentalReturnConstraints = $bindable(),\n\t\tignorePostTransitRentalReturnConstraints = $bindable(),\n\t\toptions = $bindable(),\n\t\tpreTransitProviderGroups = $bindable(),\n\t\tpostTransitProviderGroups = $bindable(),\n\t\tdirectProviderGroups = $bindable(),\n\t\thasDebug = false\n\t}: {\n\t\tone: Location;\n\t\tmaxTravelTime: number;\n\t\tserverConfig: ServerConfig | undefined;\n\t\tgeocodingBiasPlace?: maplibregl.LngLatLike;\n\t\ttime: Date;\n\t\tuseRoutedTransfers: boolean;\n\t\tpedestrianProfile: PedestrianProfile;\n\t\trequireBikeTransport: boolean;\n\t\trequireCarTransport: boolean;\n\t\ttransitModes: TransitMode[];\n\t\tmaxTransfers: number;\n\t\tpreTransitModes: PrePostDirectMode[];\n\t\tpostTransitModes: PrePostDirectMode[];\n\t\tmaxPreTransitTime: number;\n\t\tmaxPostTransitTime: number;\n\t\tarriveBy: boolean;\n\t\televationCosts: ElevationCosts;\n\t\tignorePreTransitRentalReturnConstraints: boolean;\n\t\tignorePostTransitRentalReturnConstraints: boolean;\n\t\toptions: IsochronesOptions;\n\t\tpreTransitProviderGroups: string[];\n\t\tpostTransitProviderGroups: string[];\n\t\tdirectProviderGroups: string[];\n\t\thasDebug: boolean;\n\t} = $props();\n\tconst minutesToSeconds = (n: number): number => n * 60;\n\tconst possibleMaxTravelTimes = $derived(\n\t\tgenerateTimes(\n\t\t\tminutesToSeconds(Math.min(serverConfig?.maxOneToAllTravelTimeLimit ?? 4 * 60, 6 * 60))\n\t\t).map((s) => ({\n\t\t\tvalue: s.toString(),\n\t\t\tlabel: formatDurationSec(s)\n\t\t}))\n\t);\n\n\tconst displayLevels = new Map<DisplayLevel, string>([\n\t\t['OVERLAY_RECTS', t.isochrones.canvasRects],\n\t\t['OVERLAY_CIRCLES', t.isochrones.canvasCircles],\n\t\t['GEOMETRY_CIRCLES', t.isochrones.geojsonCircles]\n\t]);\n\tconst possibleDisplayLevels = [\n\t\t...[...displayLevels.entries()].map(([id, label]) => ({ value: id, label: label }))\n\t];\n\n\tlet oneItems = $state<Array<Location>>([]);\n\n\tlet lastSearchDir = arriveBy ? 'arrival' : 'departure';\n\n\tconst getLocation = () => {\n\t\tif (navigator && navigator.geolocation) {\n\t\t\tnavigator.geolocation.getCurrentPosition(applyPosition, (e) => console.log(e), {\n\t\t\t\tenableHighAccuracy: true\n\t\t\t});\n\t\t}\n\t};\n\tconst swapPrePostData = (searchDir: string) => {\n\t\tif (searchDir != lastSearchDir) {\n\t\t\tconst tmpModes = preTransitModes;\n\t\t\tpreTransitModes = postTransitModes;\n\t\t\tpostTransitModes = tmpModes;\n\t\t\tconst tmpTime = maxPreTransitTime;\n\t\t\tmaxPreTransitTime = maxPostTransitTime;\n\t\t\tmaxPostTransitTime = tmpTime;\n\t\t\tconst tmpProviderGroups = preTransitProviderGroups;\n\t\t\tpreTransitProviderGroups = postTransitProviderGroups;\n\t\t\tpostTransitProviderGroups = tmpProviderGroups;\n\t\t\tlastSearchDir = searchDir;\n\t\t}\n\t};\n\n\tconst applyPosition = (position: { coords: { latitude: number; longitude: number } }) => {\n\t\tone = posToLocation({ lat: position.coords.latitude, lon: position.coords.longitude }, 0);\n\t};\n</script>\n\n{#snippet additionalComponents()}\n\t<div class=\"grid grid-cols-[2fr_2fr_1fr] items-center gap-2\">\n\t\t<Select.Root type=\"single\" bind:value={options.displayLevel}>\n\t\t\t<Select.Trigger class=\"overflow-hidden\" aria-label={t.isochrones.displayLevel}>\n\t\t\t\t{displayLevels.get(options.displayLevel)}\n\t\t\t</Select.Trigger>\n\t\t\t<Select.Content sideOffset={10}>\n\t\t\t\t{#each possibleDisplayLevels as level, i (i + level.value)}\n\t\t\t\t\t<Select.Item value={level.value} label={level.label}>\n\t\t\t\t\t\t{level.label}\n\t\t\t\t\t</Select.Item>\n\t\t\t\t{/each}\n\t\t\t</Select.Content>\n\t\t</Select.Root>\n\t\t<Slider.Root\n\t\t\ttype=\"single\"\n\t\t\tmin={0}\n\t\t\tmax={1000}\n\t\t\tbind:value={options.opacity}\n\t\t\tclass=\"relative flex w-full touch-none select-none items-center\"\n\t\t>\n\t\t\t<span class=\"bg-dark-10 relative h-2 w-full grow cursor-pointer overflow-hidden rounded-full\">\n\t\t\t\t<Slider.Range class=\"bg-foreground absolute h-full\" />\n\t\t\t</span>\n\t\t\t<Slider.Thumb\n\t\t\t\tindex={0}\n\t\t\t\tclass=\"border-border-input bg-background hover:border-dark-40 focus-visible:ring-foreground dark:bg-foreground dark:shadow-card focus-visible:outline-hidden block size-[25px] cursor-pointer rounded-full border shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50\"\n\t\t\t/>\n\t\t</Slider.Root>\n\t\t<input class=\"flex right-0 align-right\" type=\"color\" bind:value={options.color} />\n\t</div>\n{/snippet}\n\n<div id=\"isochrones-searchmask-container\" class=\"flex flex-col space-y-4 p-4 relative\">\n\t<AddressTypeahead\n\t\tplace={geocodingBiasPlace}\n\t\tname=\"one\"\n\t\tplaceholder={t.position}\n\t\tbind:selected={one}\n\t\tbind:items={oneItems}\n\t/>\n\t<Button\n\t\tvariant=\"ghost\"\n\t\tclass=\"absolute z-10 right-4 top-0\"\n\t\tsize=\"icon\"\n\t\tonclick={() => getLocation()}\n\t>\n\t\t<LocateFixed class=\"w-5 h-5\" />\n\t</Button>\n\t<div class=\"flex flex-row gap-2 flex-wrap\">\n\t\t<DateInput bind:value={time} />\n\t\t<RadioGroup.Root\n\t\t\tclass=\"flex\"\n\t\t\tbind:value={() => (arriveBy ? 'arrival' : 'departure'), (v) => (arriveBy = v === 'arrival')}\n\t\t\tonValueChange={swapPrePostData}\n\t\t>\n\t\t\t<Label\n\t\t\t\tfor=\"isochrones-departure\"\n\t\t\t\tclass=\"flex items-center rounded-md border-2 border-muted bg-popover p-1 px-2 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-blue-600 hover:cursor-pointer\"\n\t\t\t>\n\t\t\t\t<RadioGroup.Item\n\t\t\t\t\tvalue=\"departure\"\n\t\t\t\t\tid=\"isochrones-departure\"\n\t\t\t\t\tclass=\"sr-only\"\n\t\t\t\t\taria-label={t.departure}\n\t\t\t\t/>\n\t\t\t\t<span>{t.departure}</span>\n\t\t\t</Label>\n\t\t\t<Label\n\t\t\t\tfor=\"isochrones-arrival\"\n\t\t\t\tclass=\"flex items-center rounded-md border-2 border-muted bg-popover p-1 px-2 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-blue-600 hover:cursor-pointer\"\n\t\t\t>\n\t\t\t\t<RadioGroup.Item\n\t\t\t\t\tvalue=\"arrival\"\n\t\t\t\t\tid=\"isochrones-arrival\"\n\t\t\t\t\tclass=\"sr-only\"\n\t\t\t\t\taria-label={t.arrival}\n\t\t\t\t/>\n\t\t\t\t<span>{t.arrival}</span>\n\t\t\t</Label>\n\t\t</RadioGroup.Root>\n\t\t<AdvancedOptions\n\t\t\tbind:useRoutedTransfers\n\t\t\t{serverConfig}\n\t\t\tbind:wheelchair={\n\t\t\t\t() => pedestrianProfile === 'WHEELCHAIR',\n\t\t\t\t(v) => (pedestrianProfile = v ? 'WHEELCHAIR' : 'FOOT')\n\t\t\t}\n\t\t\tbind:requireCarTransport\n\t\t\tbind:requireBikeTransport\n\t\t\tbind:transitModes\n\t\t\tbind:maxTransfers\n\t\t\tbind:maxTravelTime\n\t\t\t{possibleMaxTravelTimes}\n\t\t\tbind:preTransitModes\n\t\t\tbind:postTransitModes\n\t\t\tdirectModes={undefined}\n\t\t\tbind:maxPreTransitTime\n\t\t\tbind:maxPostTransitTime\n\t\t\tmaxDirectTime={undefined}\n\t\t\tbind:elevationCosts\n\t\t\tbind:ignorePreTransitRentalReturnConstraints\n\t\t\tbind:ignorePostTransitRentalReturnConstraints\n\t\t\tignoreDirectRentalReturnConstraints={undefined}\n\t\t\t{additionalComponents}\n\t\t\tbind:preTransitProviderGroups\n\t\t\tbind:postTransitProviderGroups\n\t\t\tbind:directProviderGroups\n\t\t\tvia={undefined}\n\t\t\tviaMinimumStay={undefined}\n\t\t\tviaLabels={{}}\n\t\t\t{hasDebug}\n\t\t/>\n\t</div>\n</div>\n"
  },
  {
    "path": "ui/src/lib/ItineraryList.svelte",
    "content": "<script lang=\"ts\">\n\timport { Card } from '$lib/components/ui/card';\n\timport ErrorMessage from '$lib/ErrorMessage.svelte';\n\timport { Separator } from '$lib/components/ui/separator';\n\timport { formatDurationSec } from '$lib/formatDuration';\n\timport { getModeStyle, routeColor } from '$lib/modeStyle';\n\timport {\n\t\tplan,\n\t\ttype Itinerary,\n\t\ttype Leg,\n\t\ttype PlanData,\n\t\ttype PlanError,\n\t\ttype PlanResponse,\n\t\ttype Error as ApiError\n\t} from '@motis-project/motis-client';\n\timport Time from '$lib/Time.svelte';\n\timport { LoaderCircle } from '@lucide/svelte';\n\timport { t } from '$lib/i18n/translation';\n\timport DirectConnection from '$lib/DirectConnection.svelte';\n\timport type { RequestResult } from '@hey-api/client-fetch';\n\n\tlet {\n\t\troutingResponses,\n\t\tbaseResponse,\n\t\tbaseQuery,\n\t\tselectItinerary,\n\t\tupdateStartDest\n\t}: {\n\t\troutingResponses: Array<Promise<PlanResponse>>;\n\t\tbaseResponse: Promise<PlanResponse> | undefined;\n\t\tbaseQuery: PlanData | undefined;\n\t\tselectItinerary: (it: Itinerary) => void;\n\t\tupdateStartDest: (r: Awaited<RequestResult<PlanResponse, ApiError, false>>) => PlanResponse;\n\t} = $props();\n\n\tconst throwOnError = (promise: RequestResult<PlanResponse, PlanError, false>) =>\n\t\tpromise.then((res) => {\n\t\t\tif (res.error) {\n\t\t\t\tthrow { error: res.error.error, status: res.response.status };\n\t\t\t}\n\t\t\treturn res;\n\t\t});\n</script>\n\n{#snippet legSummary(l: Leg)}\n\t<div\n\t\tclass=\"flex items-center py-1 px-2 rounded-lg font-bold text-sm h-8 text-nowrap\"\n\t\tstyle={routeColor(l)}\n\t>\n\t\t<svg class=\"relative mr-1 w-4 h-4 rounded-full\">\n\t\t\t<use xlink:href={`#${getModeStyle(l)[0]}`}></use>\n\t\t</svg>\n\t\t{#if l.displayName}\n\t\t\t{l.displayName}\n\t\t{:else}\n\t\t\t{formatDurationSec(l.duration)}\n\t\t{/if}\n\t</div>\n{/snippet}\n\n{#if baseResponse}\n\t{#await baseResponse}\n\t\t<div class=\"flex items-center justify-center w-full\">\n\t\t\t<LoaderCircle class=\"animate-spin w-12 h-12 m-20\" />\n\t\t</div>\n\t{:then r}\n\t\t{#if r.direct.length !== 0}\n\t\t\t<div class=\"my-4 flex flex-wrap gap-x-3 gap-y-3\">\n\t\t\t\t{#each r.direct as d, i (i)}\n\t\t\t\t\t<DirectConnection\n\t\t\t\t\t\t{d}\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tselectItinerary(d);\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\n\t\t{#if r.itineraries.length !== 0}\n\t\t\t<div class=\"flex flex-col space-y-6 px-4 py-8\">\n\t\t\t\t{#each routingResponses as r, rI (rI)}\n\t\t\t\t\t{#await r}\n\t\t\t\t\t\t<div class=\"flex items-center justify-center w-full\">\n\t\t\t\t\t\t\t<LoaderCircle class=\"animate-spin w-12 h-12 m-20\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{:then r}\n\t\t\t\t\t\t{#if rI === 0 && baseQuery}\n\t\t\t\t\t\t\t<div class=\"w-full flex justify-between items-center space-x-4\">\n\t\t\t\t\t\t\t\t<div class=\"border-t w-full h-0\"></div>\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\troutingResponses.splice(\n\t\t\t\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t\t\t\tthrowOnError(\n\t\t\t\t\t\t\t\t\t\t\t\tplan({\n\t\t\t\t\t\t\t\t\t\t\t\t\tquery: { ...baseQuery.query, pageCursor: r.previousPageCursor }\n\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t).then(updateStartDest)\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 bg-blue-600 hover:!bg-blue-700 text-white font-bold text-sm border rounded-lg text-nowrap\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{t.earlier}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t<div class=\"border-t w-full h-0\"></div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{#each r.itineraries as it, i (i)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\tselectItinerary(it);\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Card class=\"p-4\">\n\t\t\t\t\t\t\t\t\t<div class=\"text-base flex justify-around items-start space-x-1 w-full\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"overflow-hidden basis-1/4 h-full flex flex-col\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"text-xs font-bold uppercase text-slate-400\">{t.departure}</div>\n\t\t\t\t\t\t\t\t\t\t\t<Time\n\t\t\t\t\t\t\t\t\t\t\t\tisRealtime={it.legs[0].realTime}\n\t\t\t\t\t\t\t\t\t\t\t\ttimestamp={it.startTime}\n\t\t\t\t\t\t\t\t\t\t\t\tscheduledTimestamp={it.legs[0].scheduledStartTime}\n\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"realtime-show-always\"\n\t\t\t\t\t\t\t\t\t\t\t\tqueriedTime={baseQuery?.query.time}\n\t\t\t\t\t\t\t\t\t\t\t\ttimeZone={it.legs[0].from.tz}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" />\n\t\t\t\t\t\t\t\t\t\t<div class=\"overflow-hidden basis-1/4 h-full flex flex-col\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"text-xs font-bold uppercase text-slate-400\">{t.arrival}</div>\n\t\t\t\t\t\t\t\t\t\t\t<Time\n\t\t\t\t\t\t\t\t\t\t\t\tisRealtime={it.legs[it.legs.length - 1].realTime}\n\t\t\t\t\t\t\t\t\t\t\t\ttimestamp={it.endTime}\n\t\t\t\t\t\t\t\t\t\t\t\tscheduledTimestamp={it.legs[it.legs.length - 1].scheduledEndTime}\n\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"realtime-show-always\"\n\t\t\t\t\t\t\t\t\t\t\t\tqueriedTime={it.startTime}\n\t\t\t\t\t\t\t\t\t\t\t\ttimeZone={it.legs[it.legs.length - 1].to.tz}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" />\n\t\t\t\t\t\t\t\t\t\t<div class=\"overflow-hidden basis-1/4 h-full flex flex-col\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"text-xs font-bold uppercase text-slate-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t{t.transfers}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"text-center text-nowrap\">\n\t\t\t\t\t\t\t\t\t\t\t\t{it.transfers}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" />\n\t\t\t\t\t\t\t\t\t\t<div class=\"overflow-hidden basis-1/4 h-full flex flex-col\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"text-xs font-bold uppercase text-slate-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t{t.duration}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"text-center text-nowrap\">\n\t\t\t\t\t\t\t\t\t\t\t\t{formatDurationSec(it.duration)}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<Separator class=\"my-2\" />\n\t\t\t\t\t\t\t\t\t<div class=\"mt-4 flex flex-wrap gap-x-3 gap-y-3\">\n\t\t\t\t\t\t\t\t\t\t{#each it.legs.filter((l, i) => (i == 0 && l.duration > 1) || (i == it.legs.length - 1 && l.duration > 1) || l.displayName || l.mode != 'WALK') as l, i (i)}\n\t\t\t\t\t\t\t\t\t\t\t{@render legSummary(l)}\n\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t{#if rI === routingResponses.length - 1 && baseQuery}\n\t\t\t\t\t\t\t<div class=\"w-full flex justify-between items-center space-x-4\">\n\t\t\t\t\t\t\t\t<div class=\"border-t w-full h-0\"></div>\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\t\t\t\troutingResponses.push(\n\t\t\t\t\t\t\t\t\t\t\tthrowOnError(\n\t\t\t\t\t\t\t\t\t\t\t\tplan({\n\t\t\t\t\t\t\t\t\t\t\t\t\tquery: { ...baseQuery.query, pageCursor: r.nextPageCursor }\n\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t).then(updateStartDest)\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 bg-blue-600 hover:!bg-blue-700 text-white text-sm font-bold border rounded-lg text-nowrap\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{t.later}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t<div class=\"border-t w-full h-0\"></div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t{:catch e}\n\t\t\t\t\t\t<ErrorMessage message={e.error ?? e} status={e.status ?? 404} />\n\t\t\t\t\t{/await}\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{:else if r.direct.length === 0}\n\t\t\t<ErrorMessage message={t.noItinerariesFound} status={200} />\n\t\t{/if}\n\t{:catch e}\n\t\t<ErrorMessage message={e.error ?? e} status={e.status ?? 404} />\n\t{/await}\n{/if}\n"
  },
  {
    "path": "ui/src/lib/LevelSelect.svelte",
    "content": "<script lang=\"ts\">\n\timport { Label } from '$lib/components/ui/label';\n\timport { RadioGroup, Item } from '$lib/components/ui/radio-group/index';\n\timport { lngLatToStr } from '$lib/lngLatToStr';\n\timport Control from '$lib/map/Control.svelte';\n\timport { levels } from '@motis-project/motis-client';\n\timport type { LngLatBoundsLike } from 'maplibre-gl';\n\timport maplibregl from 'maplibre-gl';\n\timport { LEVEL_MIN_ZOOM } from './constants';\n\n\tlet {\n\t\tbounds,\n\t\tzoom,\n\t\tlevel = $bindable()\n\t}: {\n\t\tbounds: LngLatBoundsLike | undefined;\n\t\tzoom: number | undefined;\n\t\tlevel: number;\n\t} = $props();\n\n\tlet value = $state('level-0');\n\tlet availableLevels: Array<string> = $state([]);\n\n\t$effect(() => {\n\t\tlevel = Number(value.substring('level-'.length));\n\t});\n\n\t$effect(() => {\n\t\tif (bounds && zoom && zoom > LEVEL_MIN_ZOOM) {\n\t\t\tconst b = maplibregl.LngLatBounds.convert(bounds);\n\t\t\tconst min = lngLatToStr(b.getNorthWest());\n\t\t\tconst max = lngLatToStr(b.getSouthEast());\n\t\t\tlevels<false>({ query: { min, max } }).then((x) => {\n\t\t\t\tavailableLevels =\n\t\t\t\t\tx.data\n\t\t\t\t\t\t?.filter((x) => {\n\t\t\t\t\t\t\treturn Number.isInteger(x);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.map((x) => String(x)) ?? [];\n\t\t\t});\n\t\t} else {\n\t\t\tavailableLevels = [];\n\t\t\tlevel = 0;\n\t\t}\n\t});\n</script>\n\n{#if availableLevels.length > 1}\n\t<Control position=\"top-right\" class=\"mb-5\">\n\t\t<RadioGroup class=\"flex flex-col items-end space-y-1\" bind:value>\n\t\t\t{#each availableLevels as l (l)}\n\t\t\t\t<Label\n\t\t\t\t\tfor={`level-${l}`}\n\t\t\t\t\tclass=\"inline-flex items-center justify-center font-bold rounded-md border-2 border-muted bg-popover h-9 w-9 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-blue-600 hover:cursor-pointer\"\n\t\t\t\t>\n\t\t\t\t\t<Item value={`level-${l}`} id={`level-${l}`} class=\"sr-only\" aria-label={`level-${l}`} />\n\t\t\t\t\t{l}\n\t\t\t\t</Label>\n\t\t\t{/each}\n\t\t</RadioGroup>\n\t</Control>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/Location.ts",
    "content": "import maplibregl from 'maplibre-gl';\nimport type { Match } from '@motis-project/motis-client';\n\nconst COORD_LVL_REGEX = /^([+-]?\\d+(\\.\\d+)?)\\s*,\\s*([+-]?\\d+(\\.\\d+)?)\\s*,\\s*([+-]?\\d+(\\.\\d+)?)$/;\nconst COORD_REGEX = /^([+-]?\\d+(\\.\\d+)?)\\s*,\\s*([+-]?\\d+(\\.\\d+)?)$/;\n\nexport type Location = {\n\tlabel: string;\n\tmatch?: Match;\n};\n\nexport const parseCoordinatesToLocation = (str?: string): Location | undefined => {\n\tif (!str) {\n\t\treturn undefined;\n\t}\n\tconst coordinateWithLevel = str.match(COORD_LVL_REGEX);\n\tif (coordinateWithLevel) {\n\t\treturn posToLocation(\n\t\t\t[Number(coordinateWithLevel[3]), Number(coordinateWithLevel[1])],\n\t\t\tNumber(coordinateWithLevel[5])\n\t\t);\n\t}\n\n\tconst coordinate = str.match(COORD_REGEX);\n\tif (coordinate) {\n\t\treturn posToLocation([Number(coordinate[3]), Number(coordinate[1])]);\n\t}\n\n\treturn undefined;\n};\n\nexport function posToLocation(pos: maplibregl.LngLatLike, level?: number): Location {\n\tconst { lat, lng } = maplibregl.LngLat.convert(pos);\n\tconst label = level == undefined ? `${lat},${lng}` : `${lat},${lng},${level}`;\n\treturn {\n\t\tlabel,\n\t\tmatch: {\n\t\t\tlat,\n\t\t\tlon: lng,\n\t\t\tlevel,\n\t\t\tid: '',\n\t\t\tareas: [],\n\t\t\ttype: 'PLACE',\n\t\t\tname: label,\n\t\t\ttokens: [],\n\t\t\tscore: 0\n\t\t}\n\t};\n}\n\nexport const parseLocation = (\n\tplace?: string | null | undefined,\n\tname?: string | null | undefined\n): Location => {\n\tif (!place || place.trim() === '') {\n\t\treturn { label: '', match: undefined };\n\t}\n\n\tconst coord = parseCoordinatesToLocation(place);\n\tif (coord) {\n\t\tif (name) {\n\t\t\tcoord.label = name;\n\t\t\tcoord.match!.name = name;\n\t\t}\n\t\treturn coord;\n\t}\n\treturn {\n\t\tlabel: name || '',\n\t\tmatch: {\n\t\t\tlat: 0.0,\n\t\t\tlon: 0.0,\n\t\t\tlevel: 0.0,\n\t\t\tid: place,\n\t\t\tareas: [],\n\t\t\ttype: 'STOP',\n\t\t\tname: name || '',\n\t\t\ttokens: [],\n\t\t\tscore: 0\n\t\t}\n\t};\n};\n"
  },
  {
    "path": "ui/src/lib/Modes.ts",
    "content": "import type { Mode, RentalFormFactor } from '@motis-project/motis-client';\n\nexport const prePostDirectModes = [\n\t'WALK',\n\t'BIKE',\n\t'CAR',\n\t'FLEX',\n\t'CAR_DROPOFF',\n\t'CAR_PARKING',\n\t'RENTAL_BICYCLE',\n\t'RENTAL_CARGO_BICYCLE',\n\t'RENTAL_CAR',\n\t'RENTAL_MOPED',\n\t'RENTAL_SCOOTER_STANDING',\n\t'RENTAL_SCOOTER_SEATED',\n\t'RENTAL_OTHER',\n\t'DEBUG_BUS_ROUTE',\n\t'DEBUG_RAILWAY_ROUTE',\n\t'DEBUG_FERRY_ROUTE'\n] as const;\nexport type PrePostDirectMode = (typeof prePostDirectModes)[number];\n\nexport const getPrePostDirectModes = (\n\tmodes: Mode[],\n\tformFactors: RentalFormFactor[]\n): PrePostDirectMode[] => {\n\treturn modes\n\t\t.filter((mode) => prePostDirectModes.includes(mode as PrePostDirectMode))\n\t\t.map((mode) => mode as PrePostDirectMode)\n\t\t.concat(formFactors.map((formFactor) => `RENTAL_${formFactor}` as PrePostDirectMode));\n};\n\nexport const getFormFactors = (modes: PrePostDirectMode[]): RentalFormFactor[] => {\n\treturn modes\n\t\t.filter((mode) => mode.startsWith('RENTAL_'))\n\t\t.map((mode) => mode.replace('RENTAL_', '')) as RentalFormFactor[];\n};\n\nexport const prePostModesToModes = (modes: PrePostDirectMode[]): Mode[] => {\n\tconst rentalMode: Mode[] = modes.some((mode) => mode.startsWith('RENTAL_')) ? ['RENTAL'] : [];\n\tconst nonRentalModes = modes.filter((mode) => !mode.startsWith('RENTAL_'));\n\treturn [...nonRentalModes, ...rentalMode].map((mode) => mode as Mode);\n};\n\nexport const possibleTransitModes = [\n\t'AIRPLANE',\n\t'HIGHSPEED_RAIL',\n\t'LONG_DISTANCE',\n\t'NIGHT_RAIL',\n\t'COACH',\n\t'RIDE_SHARING',\n\t'REGIONAL_RAIL',\n\t'SUBURBAN',\n\t'SUBWAY',\n\t'TRAM',\n\t'BUS',\n\t'FERRY',\n\t'ODM',\n\t'FUNICULAR',\n\t'AERIAL_LIFT',\n\t'OTHER'\n];\nexport type TransitMode = (typeof possibleTransitModes)[number];\n"
  },
  {
    "path": "ui/src/lib/NumberSelect.svelte",
    "content": "<script lang=\"ts\">\n\timport * as Select from '$lib/components/ui/select';\n\n\texport type NumberSelectOption = { value: string; label: string };\n\n\tlet {\n\t\tvalue = $bindable(),\n\t\tpossibleValues,\n\t\tlabelFormatter = (v) => v.toString()\n\t}: {\n\t\tvalue: number;\n\t\tpossibleValues: NumberSelectOption[];\n\t\tlabelFormatter?: (v: number) => string;\n\t} = $props();\n</script>\n\n<Select.Root\n\ttype=\"single\"\n\tbind:value={() => value.toString(), (v) => (value = parseInt(v))}\n\titems={possibleValues}\n>\n\t<Select.Trigger class=\"flex items-center w-full overflow-hidden\" aria-label=\"max travel time\">\n\t\t<div class=\"w-full text-right pr-4\">{labelFormatter(value)}</div>\n\t</Select.Trigger>\n\t<Select.Content align=\"end\">\n\t\t{#each possibleValues as option, i (i + option.value)}\n\t\t\t<Select.Item value={option.value} label={option.label}>\n\t\t\t\t<div class=\"w-full text-right pr-2\">{option.label}</div>\n\t\t\t</Select.Item>\n\t\t{/each}\n\t</Select.Content>\n</Select.Root>\n"
  },
  {
    "path": "ui/src/lib/Precision.ts",
    "content": "export const GEOCODER_PRECISION: number = 50;\nexport const ZOOM_LEVEL_PRECISION: Array<number> = [\n\t9000, // level 0 entire world\n\t8000, // level 1 entire world\n\t7000, // level 2 entire world\n\t6000, // level 3 entire world\n\t5000, // level 4 entire world\n\t4000, // level 5 entire world\n\t3000, // level 6 entire world\n\t2000, // level 7 entire world\n\t1600, // level 8 entire world\n\t800, // level 9 entire world\n\t400, // level 10 entire world\n\t200, // level 11 entire world\n\t100, // level 12 entire world\n\t50, // level 13 Darmstadt\n\t50, // level 14\n\t50, // level 15\n\t50, // level 16\n\t20, // level 17 Herrngarten\n\t10, // level 18\n\t5, // level 19\n\t5, // level 20\n\t5 // level 21\n];\n"
  },
  {
    "path": "ui/src/lib/RailViz.svelte",
    "content": "<script lang=\"ts\">\n\timport { lngLatToStr } from '$lib/lngLatToStr';\n\timport { MapboxOverlay } from '@deck.gl/mapbox';\n\timport { IconLayer } from '@deck.gl/layers';\n\timport { createTripIcon } from '$lib/map/createTripIcon';\n\timport maplibregl from 'maplibre-gl';\n\timport { onDestroy, onMount, untrack } from 'svelte';\n\timport { formatTime } from './toDateTime';\n\timport { onClickTrip } from './utils';\n\timport { getDelayColor, rgbToHex } from './Color';\n\timport type { MetaData } from './types';\n\timport Control from './map/Control.svelte';\n\timport { client } from '@motis-project/motis-client';\n\timport type { PickingInfo } from '@deck.gl/core';\n\n\tlet {\n\t\tmap,\n\t\tbounds,\n\t\tzoom,\n\t\tcolorMode\n\t}: {\n\t\tmap: maplibregl.Map | undefined;\n\t\tbounds: maplibregl.LngLatBoundsLike | undefined;\n\t\tzoom: number;\n\t\tcolorMode: 'rt' | 'route' | 'mode' | 'none';\n\t} = $props();\n\n\t//QUERY\n\tlet startTime = $state(new Date(Date.now()));\n\tlet endTime = $derived(new Date(startTime.getTime() + 180000));\n\tlet canceled = $derived(colorMode === 'none');\n\tlet query = $derived.by(() => {\n\t\tif (!bounds || !zoom) return null;\n\t\tconst b = maplibregl.LngLatBounds.convert(bounds);\n\t\tconst max = lngLatToStr(b.getNorthWest());\n\t\tconst min = lngLatToStr(b.getSouthEast());\n\t\treturn {\n\t\t\tmin,\n\t\t\tmax,\n\t\t\tstartTime: startTime.toISOString(),\n\t\t\tendTime: endTime.toISOString(),\n\t\t\tzoom,\n\t\t\tprecision: zoom >= 11 ? 5 : zoom >= 8 ? 4 : zoom >= 5 ? 3 : 2\n\t\t};\n\t});\n\n\t//TRANSFERABLES\n\tlet isProcessing = false;\n\tconst TRIPS_NUM = 12000;\n\tconst positions = new Float64Array(TRIPS_NUM * 2);\n\tconst angles = new Float32Array(TRIPS_NUM);\n\tconst colors = new Uint8Array(TRIPS_NUM * 3);\n\tconst DATA = {\n\t\tlength: TRIPS_NUM,\n\t\tpositions,\n\t\tcolors,\n\t\tangles\n\t};\n\n\t//INTERACTION\n\tconst popup = new maplibregl.Popup({\n\t\tcloseButton: false,\n\t\tcloseOnClick: true,\n\t\tmaxWidth: 'none'\n\t});\n\n\tlet activeHoverIndex: number | null = $state(null);\n\tlet clickRequested = -1;\n\n\tconst onHover = (info: PickingInfo) => {\n\t\tactiveHoverIndex = info.index;\n\t\tif (info.picked && info.index !== -1 && metadata) {\n\t\t\tcreatePopup(metadata, info.coordinate as maplibregl.LngLatLike);\n\t\t} else {\n\t\t\tmetadata = undefined;\n\t\t\tpopup.remove();\n\t\t}\n\t};\n\tconst onClick = (info: PickingInfo) => {\n\t\tif (info.picked && info.index !== -1) {\n\t\t\tif (info.index != activeHoverIndex || !metadata) {\n\t\t\t\tmetadata = undefined;\n\t\t\t\tactiveHoverIndex = info.index;\n\t\t\t\tclickRequested = info.index;\n\t\t\t} else if (metadata) {\n\t\t\t\tonClickTrip(metadata.id);\n\t\t\t}\n\t\t}\n\t};\n\tconst createPopup = (trip: MetaData, hoverCoordinate: maplibregl.LngLatLike) => {\n\t\tif (!trip || !map) return;\n\n\t\tmap.getCanvas().style.cursor = 'pointer';\n\t\tconst content = trip.realtime\n\t\t\t? `<strong>${trip.displayName}</strong><br>\n\t\t\t   <span style=\"color: ${rgbToHex(getDelayColor(trip.departureDelay, true))}\">${formatTime(new Date(trip.departure), trip.tz)}</span>\n\t\t\t   <span ${trip.departureDelay != 0 ? 'class=\"line-through\"' : ''}>${formatTime(new Date(trip.scheduledDeparture), trip.tz)}</span> ${trip.from}<br>\n\t\t\t   <span style=\"color: ${rgbToHex(getDelayColor(trip.arrivalDelay, true))}\">${formatTime(new Date(trip.arrival), trip.tz)}</span>\n\t\t\t   <span ${trip.arrivalDelay != 0 ? 'class=\"line-through\"' : ''}>${formatTime(new Date(trip.scheduledArrival), trip.tz)}</span> ${trip.to}`\n\t\t\t: `<strong>${trip.displayName}</strong><br>\n\t\t\t   ${formatTime(new Date(trip.departure), trip.tz)} ${trip.from}<br>\n\t\t\t   ${formatTime(new Date(trip.arrival), trip.tz)} ${trip.to}`;\n\n\t\tpopup.setLngLat(hoverCoordinate).setHTML(content).addTo(map);\n\t};\n\n\t//ANIMATION\n\tconst TripIcon = createTripIcon(128);\n\tconst IconMapping = {\n\t\tmarker: {\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: 128,\n\t\t\theight: 128,\n\t\t\tanchorY: 64,\n\t\t\tanchorX: 64,\n\t\t\tmask: true\n\t\t}\n\t};\n\tconst createLayer = () => {\n\t\tif (!DATA.positions || DATA.positions.byteLength === 0) return;\n\t\treturn new IconLayer({\n\t\t\tid: 'trips-layer',\n\t\t\tdata: {\n\t\t\t\tlength: DATA.length,\n\t\t\t\tattributes: {\n\t\t\t\t\tgetPosition: { value: DATA.positions, size: 2 },\n\t\t\t\t\tgetAngle: { value: DATA.angles, size: 1 },\n\t\t\t\t\tgetColor: { value: DATA.colors, size: 3, normalized: true }\n\t\t\t\t}\n\t\t\t},\n\t\t\tbeforeId: 'road-name-text',\n\t\t\t// @ts-expect-error: canvas element seems to work fine\n\t\t\ticonAtlas: TripIcon,\n\t\t\ticonMapping: IconMapping,\n\t\t\tpickable: colorMode != 'none',\n\t\t\tsizeScale: 5,\n\t\t\tgetSize: 10,\n\t\t\tgetIcon: (_) => 'marker',\n\t\t\tcolorFormat: 'RGB',\n\t\t\tvisible: colorMode !== 'none',\n\t\t\tuseDevicePixels: false,\n\t\t\tparameters: { depthTest: false },\n\t\t\tonClick,\n\t\t\tonHover\n\t\t});\n\t};\n\tlet animationId: number;\n\tconst animate = () => {\n\t\tif (!DATA.positions || DATA.positions.length === 0) return;\n\t\tworker.postMessage(\n\t\t\t{\n\t\t\t\ttype: 'update',\n\t\t\t\tcolorMode,\n\t\t\t\tpositions: DATA.positions,\n\t\t\t\tindex: activeHoverIndex,\n\t\t\t\tangles: DATA.angles,\n\t\t\t\tcolors: DATA.colors,\n\t\t\t\tlength: DATA.length\n\t\t\t},\n\t\t\t[DATA.positions.buffer, DATA.angles.buffer, DATA.colors.buffer]\n\t\t);\n\t};\n\n\t// UPDATE\n\t$effect(() => {\n\t\tif (!query || isProcessing || canceled) return;\n\t\tuntrack(() => {\n\t\t\tisProcessing = true;\n\t\t\tworker.postMessage({ type: 'fetch', query });\n\t\t});\n\t});\n\tsetInterval(() => {\n\t\tif (query && colorMode !== 'none') {\n\t\t\tstartTime = new Date();\n\t\t}\n\t}, 60000);\n\n\t//SETUP\n\tlet status = $state();\n\tlet overlay: MapboxOverlay;\n\tlet worker: Worker;\n\tlet metadata: MetaData | undefined = $state();\n\n\tonMount(() => {\n\t\tworker = new Worker(new URL('tripsWorker.ts', import.meta.url), { type: 'module' });\n\t\tworker.postMessage({ type: 'init', baseUrl: client.getConfig().baseUrl });\n\t\tworker.onmessage = (e) => {\n\t\t\tif (e.data.type == 'fetch-complete') {\n\t\t\t\tstatus = e.data.status;\n\t\t\t\tactiveHoverIndex = -1;\n\t\t\t\tisProcessing = false;\n\t\t\t} else {\n\t\t\t\tconst { positions, angles, length, colors } = e.data;\n\t\t\t\tDATA.positions = new Float64Array(positions.buffer);\n\t\t\t\tDATA.angles = new Float32Array(angles.buffer);\n\t\t\t\tDATA.colors = new Uint8Array(colors.buffer);\n\t\t\t\tDATA.length = length;\n\t\t\t\tmetadata = e.data.metadata;\n\t\t\t\tif (clickRequested != -1 && e.data.metadataIndex == clickRequested && metadata) {\n\t\t\t\t\tonClickTrip(metadata.id);\n\t\t\t\t\tclickRequested = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t\toverlay.setProps({ layers: [createLayer()] });\n\t\t\tif (canceled) {\n\t\t\t\tcancelAnimationFrame(animationId);\n\t\t\t} else {\n\t\t\t\tanimationId = requestAnimationFrame(animate);\n\t\t\t}\n\t\t};\n\t\toverlay = new MapboxOverlay({\n\t\t\tinterleaved: true\n\t\t});\n\t});\n\t$effect(() => {\n\t\tif (!map || !overlay) return;\n\t\tmap.addControl(overlay);\n\t});\n\tonDestroy(() => {\n\t\tif (animationId) cancelAnimationFrame(animationId);\n\t\tif (overlay) map?.removeControl(overlay);\n\t\tworker.terminate();\n\t\tpopup.remove();\n\t});\n</script>\n\n{#if status && status !== 200}\n\t<Control position=\"bottom-left\">trips response status: {status}</Control>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/Route.svelte",
    "content": "<script lang=\"ts\">\n\timport { getModeStyle, routeColor, type LegLike } from './modeStyle';\n\timport { cn } from './utils';\n\n\tconst {\n\t\tl,\n\t\tclass: className,\n\t\tonClickTrip\n\t}: {\n\t\tl: LegLike;\n\t\tclass?: string;\n\t\tonClickTrip: (tripId: string) => void;\n\t} = $props();\n\n\tconst modeIcon = $derived(getModeStyle(l)[0]);\n</script>\n\n<button\n\tclass={cn(\n\t\t'flex items-center text-nowrap rounded-full pl-2 pr-1 h-8 font-bold',\n\t\tclassName,\n\t\tl.displayName ? 'pr-3' : undefined\n\t)}\n\tstyle={routeColor(l)}\n\tonclick={() => {\n\t\tif (l.tripId) {\n\t\t\tonClickTrip(l.tripId);\n\t\t} else {\n\t\t\tconsole.log('tripId missing', l);\n\t\t}\n\t}}\n>\n\t<svg class=\"relative mr-2 min-w-6 min-h-6 max-w-6 max-h-6 rounded-full\">\n\t\t<use xlink:href={`#${modeIcon}`}></use>\n\t</svg>\n\t<div class=\"text-center\">\n\t\t{l.displayName}\n\t</div>\n</button>\n"
  },
  {
    "path": "ui/src/lib/SearchMask.svelte",
    "content": "<script lang=\"ts\">\n\timport { t } from '$lib/i18n/translation';\n\timport { ArrowUpDown, LocateFixed } from '@lucide/svelte';\n\timport maplibregl from 'maplibre-gl';\n\timport * as RadioGroup from '$lib/components/ui/radio-group';\n\timport Button from '$lib/components/ui/button/button.svelte';\n\timport { Label } from '$lib/components/ui/label';\n\timport type {\n\t\tElevationCosts,\n\t\tMode,\n\t\tPedestrianProfile,\n\t\tServerConfig\n\t} from '@motis-project/motis-client';\n\timport AddressTypeahead from '$lib/AddressTypeahead.svelte';\n\timport AdvancedOptions from '$lib/AdvancedOptions.svelte';\n\timport DateInput from '$lib/DateInput.svelte';\n\timport { posToLocation, type Location } from '$lib/Location';\n\timport type { PrePostDirectMode } from '$lib/Modes';\n\n\tlet {\n\t\tgeocodingBiasPlace,\n\t\tserverConfig,\n\t\tfrom = $bindable(),\n\t\tto = $bindable(),\n\t\ttime = $bindable(),\n\t\tarriveBy = $bindable(),\n\t\tpedestrianProfile = $bindable(),\n\t\tuseRoutedTransfers = $bindable(),\n\t\tmaxTransfers = $bindable(),\n\t\trequireCarTransport = $bindable(),\n\t\trequireBikeTransport = $bindable(),\n\t\ttransitModes = $bindable(),\n\t\tpreTransitModes = $bindable(),\n\t\tpostTransitModes = $bindable(),\n\t\tdirectModes = $bindable(),\n\t\televationCosts = $bindable(),\n\t\tmaxPreTransitTime = $bindable(),\n\t\tmaxPostTransitTime = $bindable(),\n\t\tmaxDirectTime = $bindable(),\n\t\tignorePreTransitRentalReturnConstraints = $bindable(),\n\t\tignorePostTransitRentalReturnConstraints = $bindable(),\n\t\tignoreDirectRentalReturnConstraints = $bindable(),\n\t\tpreTransitProviderGroups = $bindable(),\n\t\tpostTransitProviderGroups = $bindable(),\n\t\tdirectProviderGroups = $bindable(),\n\t\tvia = $bindable(),\n\t\tviaMinimumStay = $bindable(),\n\t\tviaLabels = $bindable(),\n\t\thasDebug = false\n\t}: {\n\t\tgeocodingBiasPlace?: maplibregl.LngLatLike;\n\t\tserverConfig: ServerConfig | undefined;\n\t\tfrom: Location;\n\t\tto: Location;\n\t\ttime: Date;\n\t\tarriveBy: boolean;\n\t\tpedestrianProfile: PedestrianProfile;\n\t\tuseRoutedTransfers: boolean;\n\t\tmaxTransfers: number;\n\t\trequireCarTransport: boolean;\n\t\trequireBikeTransport: boolean;\n\t\ttransitModes: Mode[];\n\t\tpreTransitModes: PrePostDirectMode[];\n\t\tpostTransitModes: PrePostDirectMode[];\n\t\tdirectModes: PrePostDirectMode[];\n\t\televationCosts: ElevationCosts;\n\t\tmaxPreTransitTime: number;\n\t\tmaxPostTransitTime: number;\n\t\tmaxDirectTime: number;\n\t\tignorePreTransitRentalReturnConstraints: boolean;\n\t\tignorePostTransitRentalReturnConstraints: boolean;\n\t\tignoreDirectRentalReturnConstraints: boolean;\n\t\tpreTransitProviderGroups: string[];\n\t\tpostTransitProviderGroups: string[];\n\t\tdirectProviderGroups: string[];\n\t\tvia: undefined | Location[];\n\t\tviaMinimumStay: undefined | number[];\n\t\tviaLabels: Record<string, string>;\n\t\thasDebug: boolean;\n\t} = $props();\n\n\tlet fromItems = $state<Array<Location>>([]);\n\tlet toItems = $state<Array<Location>>([]);\n\n\tconst getLocation = () => {\n\t\tif (navigator && navigator.geolocation) {\n\t\t\tnavigator.geolocation.getCurrentPosition(applyPosition, (e) => console.log(e), {\n\t\t\t\tenableHighAccuracy: true\n\t\t\t});\n\t\t}\n\t};\n\n\tconst applyPosition = (position: { coords: { latitude: number; longitude: number } }) => {\n\t\tfrom = posToLocation({ lat: position.coords.latitude, lon: position.coords.longitude }, 0);\n\t};\n</script>\n\n<div id=\"searchmask-container\" class=\"flex flex-col space-y-4 p-4 relative\">\n\t<AddressTypeahead\n\t\tplace={geocodingBiasPlace}\n\t\tname=\"from\"\n\t\tplaceholder={t.from}\n\t\tbind:selected={from}\n\t\tbind:items={fromItems}\n\t\t{transitModes}\n\t/>\n\t<AddressTypeahead\n\t\tplace={geocodingBiasPlace}\n\t\tname=\"to\"\n\t\tplaceholder={t.to}\n\t\tbind:selected={to}\n\t\tbind:items={toItems}\n\t\t{transitModes}\n\t/>\n\t<Button\n\t\tvariant=\"ghost\"\n\t\tclass=\"absolute z-10 right-4 top-0\"\n\t\tsize=\"icon\"\n\t\tonclick={() => getLocation()}\n\t>\n\t\t<LocateFixed class=\"w-5 h-5\" />\n\t</Button>\n\t<Button\n\t\tclass=\"absolute z-10 right-14 top-6\"\n\t\tvariant=\"outline\"\n\t\tsize=\"icon\"\n\t\tonclick={() => {\n\t\t\tconst tmp = to;\n\t\t\tto = from;\n\t\t\tfrom = tmp;\n\n\t\t\tconst tmpItems = toItems;\n\t\t\ttoItems = fromItems;\n\t\t\tfromItems = tmpItems;\n\t\t}}\n\t>\n\t\t<ArrowUpDown class=\"w-5 h-5\" />\n\t</Button>\n\t<div class=\"flex flex-row gap-2 flex-wrap\">\n\t\t<DateInput bind:value={time} />\n\t\t<RadioGroup.Root\n\t\t\tclass=\"flex\"\n\t\t\tbind:value={() => (arriveBy ? 'arrival' : 'departure'), (v) => (arriveBy = v === 'arrival')}\n\t\t>\n\t\t\t<Label\n\t\t\t\tfor=\"departure\"\n\t\t\t\tclass=\"flex items-center rounded-md border-2 border-muted bg-popover p-1 px-2 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-blue-600 hover:cursor-pointer\"\n\t\t\t>\n\t\t\t\t<RadioGroup.Item\n\t\t\t\t\tvalue=\"departure\"\n\t\t\t\t\tid=\"departure\"\n\t\t\t\t\tclass=\"sr-only\"\n\t\t\t\t\taria-label={t.departure}\n\t\t\t\t/>\n\t\t\t\t<span>{t.departure}</span>\n\t\t\t</Label>\n\t\t\t<Label\n\t\t\t\tfor=\"arrival\"\n\t\t\t\tclass=\"flex items-center rounded-md border-2 border-muted bg-popover p-1 px-2 hover:bg-accent hover:text-accent-foreground [&:has([data-state=checked])]:border-blue-600 hover:cursor-pointer\"\n\t\t\t>\n\t\t\t\t<RadioGroup.Item value=\"arrival\" id=\"arrival\" class=\"sr-only\" aria-label={t.arrival} />\n\t\t\t\t<span>{t.arrival}</span>\n\t\t\t</Label>\n\t\t</RadioGroup.Root>\n\t\t<AdvancedOptions\n\t\t\t{serverConfig}\n\t\t\tbind:useRoutedTransfers\n\t\t\tbind:wheelchair={\n\t\t\t\t() => pedestrianProfile === 'WHEELCHAIR',\n\t\t\t\t(v) => (pedestrianProfile = v ? 'WHEELCHAIR' : 'FOOT')\n\t\t\t}\n\t\t\tbind:requireCarTransport\n\t\t\tbind:maxTransfers\n\t\t\tmaxTravelTime={undefined}\n\t\t\tbind:requireBikeTransport\n\t\t\tbind:transitModes\n\t\t\tbind:preTransitModes\n\t\t\tbind:postTransitModes\n\t\t\tbind:directModes\n\t\t\tbind:maxPreTransitTime\n\t\t\tbind:maxPostTransitTime\n\t\t\tbind:maxDirectTime\n\t\t\tbind:elevationCosts\n\t\t\tbind:ignorePreTransitRentalReturnConstraints\n\t\t\tbind:ignorePostTransitRentalReturnConstraints\n\t\t\tbind:ignoreDirectRentalReturnConstraints\n\t\t\tbind:preTransitProviderGroups\n\t\t\tbind:postTransitProviderGroups\n\t\t\tbind:directProviderGroups\n\t\t\tbind:via\n\t\t\tbind:viaMinimumStay\n\t\t\tbind:viaLabels\n\t\t\t{hasDebug}\n\t\t/>\n\t</div>\n</div>\n"
  },
  {
    "path": "ui/src/lib/StopTimes.svelte",
    "content": "<script lang=\"ts\">\n\timport {\n\t\tstoptimes,\n\t\ttype StoptimesError,\n\t\ttype StoptimesResponse\n\t} from '@motis-project/motis-client';\n\timport { LoaderCircle, ArrowRight, CircleX } from '@lucide/svelte';\n\timport ErrorMessage from '$lib/ErrorMessage.svelte';\n\timport Time from '$lib/Time.svelte';\n\timport Route from '$lib/Route.svelte';\n\timport { Button } from '$lib/components/ui/button';\n\timport { language, t } from '$lib/i18n/translation';\n\timport type { RequestResult } from '@hey-api/client-fetch';\n\timport { onClickStop, onClickTrip } from '$lib/utils';\n\timport { getModeLabel } from './map/getModeLabel';\n\timport { posToLocation } from './Location';\n\timport type { Location } from './Location';\n\timport maplibregl from 'maplibre-gl';\n\timport Alerts from './Alerts.svelte';\n\n\tlet {\n\t\tstopId,\n\t\tstopName,\n\t\ttime: queryTime,\n\t\tstopNameFromResponse = $bindable(),\n\t\tstop = $bindable(),\n\t\tstopMarker = $bindable(),\n\t\tarriveBy\n\t}: {\n\t\tstopId: string;\n\t\tstopName: string;\n\t\ttime: Date;\n\t\tarriveBy?: boolean;\n\t\tstopNameFromResponse: string;\n\t\tstop: Location | undefined;\n\t\tstopMarker: maplibregl.Marker | undefined;\n\t} = $props();\n\n\tlet query = $derived({\n\t\tstopId,\n\t\ttime: queryTime.toISOString(),\n\t\tarriveBy,\n\t\tn: 10,\n\t\texactRadius: false,\n\t\tradius: 200,\n\t\tlanguage: [language]\n\t});\n\t/* eslint-disable svelte/prefer-writable-derived */\n\tlet responses = $state<Array<Promise<StoptimesResponse>>>([]);\n\t$effect(() => {\n\t\tresponses = [throwOnError(stoptimes({ query }))];\n\t});\n\t/* eslint-enable svelte/prefer-writable-derived */\n\n\tconst throwOnError = (promise: RequestResult<StoptimesResponse, StoptimesError, false>) =>\n\t\tpromise.then((response) => {\n\t\t\tif (response.error) {\n\t\t\t\tconsole.log(response.error);\n\t\t\t\tthrow { error: response.error.error, status: response.response.status };\n\t\t\t}\n\t\t\tstopNameFromResponse = response.data?.place?.name || '';\n\t\t\tlet placeFromResponse = response.data?.place;\n\t\t\tstop = posToLocation(\n\t\t\t\tmaplibregl.LngLat.convert([placeFromResponse.lon, placeFromResponse.lat])\n\t\t\t);\n\t\t\tstopMarker?.setLngLat(stop.match!);\n\t\t\treturn response.data!;\n\t\t});\n</script>\n\n<div class=\"flex justify-center mb-4\">\n\t<Button\n\t\tclass=\"font-bold\"\n\t\tvariant=\"outline\"\n\t\tonclick={() => {\n\t\t\tonClickStop(stopName, stopId, queryTime, !arriveBy);\n\t\t}}\n\t>\n\t\t{#if arriveBy}\n\t\t\t{t.switchToDepartures}\n\t\t{:else}\n\t\t\t{t.switchToArrivals}\n\t\t{/if}\n\t</Button>\n</div>\n{#each responses as r, rI (rI)}\n\t{#await r}\n\t\t<div class=\"flex items-center justify-center\">\n\t\t\t<LoaderCircle\n\t\t\t\tclass=\"animate-spin w-20 h-20 my-60\n\t\t\t\t\"\n\t\t\t/>\n\t\t</div>\n\t{:then r}\n\t\t{#if rI === 0 && r.previousPageCursor.length}\n\t\t\t<div class=\"col-span-full flex justify-center items-center border-b pb-4\">\n\t\t\t\t<button\n\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\tresponses.splice(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\tthrowOnError(stoptimes({ query: { ...query, pageCursor: r.previousPageCursor } }))\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t\tclass=\"px-2 py-1 bg-blue-600 hover:!bg-blue-700 text-white font-bold text-sm border rounded-lg text-nowrap\"\n\t\t\t\t>\n\t\t\t\t\t{t.earlier}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t{/if}\n\t\t{#each r.stopTimes as stopTime, i (i)}\n\t\t\t{@const timestamp = arriveBy ? stopTime.place.arrival! : stopTime.place.departure!}\n\t\t\t{@const scheduledTimestamp = arriveBy\n\t\t\t\t? stopTime.place.scheduledArrival!\n\t\t\t\t: stopTime.place.scheduledDeparture!}\n\n\t\t\t<div\n\t\t\t\tclass=\"gap-y-2 p-3 w-full text-base grid grid-cols-[auto_1fr] border-b duration-500 ease-out transition-all\"\n\t\t\t>\n\t\t\t\t<div class=\"flex flex-col w-24\">\n\t\t\t\t\t<Time\n\t\t\t\t\t\tvariant=\"schedule\"\n\t\t\t\t\t\ttimeZone={stopTime.place.tz}\n\t\t\t\t\t\tisRealtime={stopTime.realTime}\n\t\t\t\t\t\t{timestamp}\n\t\t\t\t\t\t{scheduledTimestamp}\n\t\t\t\t\t\tqueriedTime={queryTime.toISOString()}\n\t\t\t\t\t\t{arriveBy}\n\t\t\t\t\t/>\n\t\t\t\t\t<Time\n\t\t\t\t\t\tvariant=\"realtime\"\n\t\t\t\t\t\ttimeZone={stopTime.place.tz}\n\t\t\t\t\t\tisRealtime={stopTime.realTime}\n\t\t\t\t\t\t{timestamp}\n\t\t\t\t\t\t{scheduledTimestamp}\n\t\t\t\t\t\t{arriveBy}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex-col text-base\">\n\t\t\t\t\t<div class=\"flex justify-between\">\n\t\t\t\t\t\t<Route class=\"text-ellipsis mb-2\" l={stopTime} {onClickTrip} />\n\t\t\t\t\t\t{#if stopTime.place.track}\n\t\t\t\t\t\t\t<span class=\"text-nowrap ml-3 h-fit px-1 text-base border font-semibold rounded-lg\">\n\t\t\t\t\t\t\t\t{getModeLabel(stopTime.mode) == 'Track' ? t.trackAbr : t.platformAbr}\n\t\t\t\t\t\t\t\t{stopTime.place.track}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"flex items-center justify-between\">\n\t\t\t\t\t\t<div class=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t<ArrowRight class=\"shrink-0 stroke-muted-foreground h-4 w-4\" />\n\t\t\t\t\t\t\t<span class=\"leading-none\">\n\t\t\t\t\t\t\t\t{stopTime.headsign}\n\t\t\t\t\t\t\t\t{#if !stopTime.headsign}\n\t\t\t\t\t\t\t\t\t{stopTime.tripTo.name}\n\t\t\t\t\t\t\t\t{:else if !stopTime.tripTo.name.startsWith(stopTime.headsign)}\n\t\t\t\t\t\t\t\t\t<span class=\"stroke-muted-foreground\">({stopTime.tripTo.name})</span>\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Alerts tz={stopTime.place.tz} alerts={stopTime.place.alerts} />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{#if stopTime.pickupDropoffType == 'NOT_ALLOWED'}\n\t\t\t\t\t<div class=\"flex ml-24 items-center col-span-full text-destructive text-sm\">\n\t\t\t\t\t\t<CircleX class=\"stroke-destructive h-4 w-4\" />\n\t\t\t\t\t\t<span class=\"ml-1 leading-none\">\n\t\t\t\t\t\t\t{stopTime.tripCancelled\n\t\t\t\t\t\t\t\t? t.tripCancelled\n\t\t\t\t\t\t\t\t: stopTime.cancelled\n\t\t\t\t\t\t\t\t\t? t.stopCancelled\n\t\t\t\t\t\t\t\t\t: arriveBy\n\t\t\t\t\t\t\t\t\t\t? t.outDisallowed\n\t\t\t\t\t\t\t\t\t\t: t.inDisallowed}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t{/each}\n\t\t{#if !r.stopTimes.length}\n\t\t\t<div class=\"col-span-full w-full flex items-center justify-center\">\n\t\t\t\t<ErrorMessage message={t.noItinerariesFound} status={404} />\n\t\t\t</div>\n\t\t{/if}\n\n\t\t{#if rI === responses.length - 1 && r.nextPageCursor.length}\n\t\t\t<div class=\"col-span-full flex mt-4 justify-center items-center\">\n\t\t\t\t<button\n\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\tresponses.push(\n\t\t\t\t\t\t\tthrowOnError(stoptimes({ query: { ...query, pageCursor: r.nextPageCursor } }))\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t\tclass=\"px-2 py-1 bg-blue-600 hover:!bg-blue-700 text-white text-sm font-bold border rounded-lg text-nowrap\"\n\t\t\t\t>\n\t\t\t\t\t{t.later}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t{/if}\n\t{:catch e}\n\t\t<div class=\"col-span-full w-full flex items-center justify-center\">\n\t\t\t<ErrorMessage message={e.error} status={e.status} />\n\t\t</div>\n\t{/await}\n{/each}\n"
  },
  {
    "path": "ui/src/lib/StreetModes.svelte",
    "content": "<script lang=\"ts\">\n\timport { t } from '$lib/i18n/translation';\n\timport { cn } from '$lib/utils';\n\timport * as Select from '$lib/components/ui/select';\n\timport { Switch } from '$lib/components/ui/switch';\n\timport { formatDurationSec } from '$lib/formatDuration';\n\timport { type PrePostDirectMode, getFormFactors } from '$lib/Modes';\n\timport { formFactorAssets } from '$lib/map/rentals/assets';\n\timport { DEFAULT_COLOR } from '$lib/map/rentals/style';\n\timport { rentals, type RentalFormFactor } from '@motis-project/motis-client';\n\timport { createQuery } from '@tanstack/svelte-query';\n\n\tlet {\n\t\tlabel,\n\t\tdisabled,\n\t\tmodes = $bindable(),\n\t\tmaxTransitTime = $bindable(),\n\t\tpossibleModes,\n\t\tpossibleMaxTransitTime,\n\t\tignoreRentalReturnConstraints = $bindable(),\n\t\tproviderGroups = $bindable()\n\t}: {\n\t\tlabel: string;\n\t\tdisabled?: boolean;\n\t\tmodes: PrePostDirectMode[];\n\t\tmaxTransitTime: number;\n\t\tpossibleModes: readonly PrePostDirectMode[];\n\t\tpossibleMaxTransitTime: number[];\n\t\tignoreRentalReturnConstraints: boolean;\n\t\tproviderGroups: string[];\n\t} = $props();\n\n\ttype TranslationKey = keyof typeof t;\n\n\tconst availableModes = possibleModes.map((value) => ({\n\t\tvalue,\n\t\tlabel: t[value as TranslationKey] as string\n\t}));\n\n\ttype ProviderOption = {\n\t\tid: string;\n\t\tname: string;\n\t\tcolor: string;\n\t\tformFactors: RentalFormFactor[];\n\t};\n\n\tconst providerGroupsQuery = createQuery(() => ({\n\t\tqueryKey: ['rentalProviderGroups'],\n\t\tqueryFn: async () => {\n\t\t\tconst { data, error } = await rentals({ query: { withProviders: false } });\n\t\t\tif (error) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn data;\n\t\t},\n\t\tstaleTime: 0\n\t}));\n\n\tconst allProviderGroupOptions = $derived.by((): ProviderOption[] => {\n\t\treturn (providerGroupsQuery.data?.providerGroups || [])\n\t\t\t.map((group) => ({\n\t\t\t\tid: group.id,\n\t\t\t\tname: group.name,\n\t\t\t\tcolor: group.color ?? DEFAULT_COLOR,\n\t\t\t\tformFactors: group.formFactors\n\t\t\t}))\n\t\t\t.sort((a, b) => a.name.localeCompare(b.name));\n\t});\n\n\tconst selectedRentalFormFactors = $derived(getFormFactors(modes));\n\n\tconst providerGroupOptions = $derived.by((): ProviderOption[] => {\n\t\tif (selectedRentalFormFactors.length === 0) {\n\t\t\treturn allProviderGroupOptions;\n\t\t}\n\t\treturn allProviderGroupOptions.filter((option) => {\n\t\t\treturn option.formFactors.some((formFactor) =>\n\t\t\t\tselectedRentalFormFactors.includes(formFactor)\n\t\t\t);\n\t\t});\n\t});\n\n\tconst containsRental = (modes: PrePostDirectMode[]) =>\n\t\tmodes.some((mode) => mode.startsWith('RENTAL_'));\n\n\tconst showRental = $derived(containsRental(modes));\n\n\t$effect(() => {\n\t\tif (!providerGroupOptions.length) {\n\t\t\treturn;\n\t\t}\n\t\tconst deduped = Array.from(new Set(providerGroups));\n\t\tif (deduped.length !== providerGroups.length) {\n\t\t\tproviderGroups = deduped;\n\t\t\treturn;\n\t\t}\n\t\tconst validIds = new Set(providerGroupOptions.map((option) => option.id));\n\t\tconst filtered = deduped.filter((id) => validIds.has(id));\n\t\tif (filtered.length !== deduped.length) {\n\t\t\tproviderGroups = filtered;\n\t\t\treturn;\n\t\t}\n\t\tif (filtered.length && filtered.length === validIds.size) {\n\t\t\tproviderGroups = [];\n\t\t}\n\t});\n\n\tconst selectedModesLabel = $derived(\n\t\tavailableModes\n\t\t\t.filter((m) => modes?.includes(m.value))\n\t\t\t.map((m) => m.label)\n\t\t\t.join(', ')\n\t);\n\n\tconst selectedProviderGroupsLabel = $derived.by(() => {\n\t\tconst names = providerGroupOptions\n\t\t\t.filter((option) => providerGroups.includes(option.id))\n\t\t\t.map((option) => option.name);\n\t\treturn names.length ? names.join(', ') : t.defaultSelectedProviders;\n\t});\n</script>\n\n<div class=\"grid grid-cols-[1fr_2fr_1fr] items-center gap-2\">\n\t<div class=\"text-sm\">\n\t\t{label}\n\t</div>\n\t<Select.Root {disabled} type=\"multiple\" bind:value={modes}>\n\t\t<Select.Trigger class=\"flex items-center w-full overflow-hidden\" aria-label={label}>\n\t\t\t<span>{selectedModesLabel}</span>\n\t\t</Select.Trigger>\n\t\t<Select.Content sideOffset={10}>\n\t\t\t{#each possibleModes as mode, i (i + mode)}\n\t\t\t\t<Select.Item value={mode} label={t[mode as TranslationKey] as string}>\n\t\t\t\t\t{t[mode as TranslationKey]}\n\t\t\t\t</Select.Item>\n\t\t\t{/each}\n\t\t</Select.Content>\n\t</Select.Root>\n\t<Select.Root\n\t\t{disabled}\n\t\ttype=\"single\"\n\t\tbind:value={() => maxTransitTime.toString(), (v) => (maxTransitTime = parseInt(v))}\n\t>\n\t\t<Select.Trigger\n\t\t\tclass=\"flex items-center w-full overflow-hidden\"\n\t\t\taria-label={t.routingSegments.maxPreTransitTime}\n\t\t>\n\t\t\t{formatDurationSec(maxTransitTime)}\n\t\t</Select.Trigger>\n\t\t<Select.Content sideOffset={10}>\n\t\t\t{#each possibleMaxTransitTime as duration (duration)}\n\t\t\t\t<Select.Item value={`${duration}`} label={formatDurationSec(duration)}>\n\t\t\t\t\t{formatDurationSec(duration)}\n\t\t\t\t</Select.Item>\n\t\t\t{/each}\n\t\t</Select.Content>\n\t</Select.Root>\n\t<div class={cn('text-sm', showRental || 'hidden')}>\n\t\t{t.sharingProviders}\n\t</div>\n\t<div class={cn('col-span-2 col-start-2', showRental || 'hidden')}>\n\t\t<Select.Root {disabled} type=\"multiple\" bind:value={providerGroups}>\n\t\t\t<Select.Trigger\n\t\t\t\tclass=\"flex items-center w-full overflow-hidden\"\n\t\t\t\taria-label={t.sharingProviders}\n\t\t\t>\n\t\t\t\t<span>{selectedProviderGroupsLabel}</span>\n\t\t\t</Select.Trigger>\n\t\t\t<Select.Content sideOffset={10} class=\"max-w-[100svw]\">\n\t\t\t\t{#each providerGroupOptions as option (option.id)}\n\t\t\t\t\t<Select.Item value={option.id} label={option.name}>\n\t\t\t\t\t\t<div class=\"flex w-full items-center justify-between gap-2\">\n\t\t\t\t\t\t\t<span class=\"truncate\">{option.name}</span>\n\t\t\t\t\t\t\t{#if option.formFactors.length}\n\t\t\t\t\t\t\t\t<div class=\"flex items-center gap-1\" style={`color: ${option.color}`}>\n\t\t\t\t\t\t\t\t\t{#each Array.from(new Set(option.formFactors.map((ff) => formFactorAssets[ff].svg))).sort() as icon (icon)}\n\t\t\t\t\t\t\t\t\t\t<svg class=\"h-4 w-4 fill-current\" aria-hidden=\"true\" focusable=\"false\">\n\t\t\t\t\t\t\t\t\t\t\t<use href={`#${icon}`} />\n\t\t\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Select.Item>\n\t\t\t\t{/each}\n\t\t\t</Select.Content>\n\t\t</Select.Root>\n\t</div>\n\t<div class={cn('col-span-2 col-start-2', showRental || 'hidden')}>\n\t\t<Switch\n\t\t\t{disabled}\n\t\t\tbind:checked={\n\t\t\t\t() => !ignoreRentalReturnConstraints, (v) => (ignoreRentalReturnConstraints = !v)\n\t\t\t}\n\t\t\tlabel={t.considerRentalReturnConstraints}\n\t\t\tid=\"ignorePreTransitRentalReturnConstraints\"\n\t\t/>\n\t</div>\n</div>\n"
  },
  {
    "path": "ui/src/lib/Time.svelte",
    "content": "<script lang=\"ts\">\n\timport { language } from '$lib/i18n/translation';\n\timport { formatTime } from './toDateTime';\n\timport { cn } from './utils';\n\n\tlet {\n\t\tclass: className,\n\t\ttimestamp,\n\t\tscheduledTimestamp,\n\t\tisRealtime,\n\t\tvariant,\n\t\tqueriedTime,\n\t\ttimeZone,\n\t\tarriveBy\n\t}: {\n\t\tclass?: string;\n\t\ttimestamp: string;\n\t\tscheduledTimestamp: string;\n\t\tisRealtime: boolean;\n\t\tvariant: 'schedule' | 'realtime' | 'realtime-show-always';\n\t\tqueriedTime?: string | undefined;\n\t\ttimeZone: string | undefined;\n\t\tarriveBy?: boolean | undefined;\n\t} = $props();\n\n\tconst t = $derived(new Date(timestamp));\n\tconst scheduled = $derived(new Date(scheduledTimestamp));\n\tconst delayMinutes = $derived((t.getTime() - scheduled.getTime()) / 60000);\n\tconst highDelay = $derived(isRealtime && delayMinutes > 3);\n\tconst lowDelay = $derived(isRealtime && delayMinutes <= 3);\n\tconst early = $derived(isRealtime && delayMinutes <= -1);\n\tconst notOnTime = $derived(arriveBy ? highDelay : highDelay || early);\n\tconst roughlyOnTime = $derived(arriveBy ? lowDelay : lowDelay && !early);\n\tconst isValidDate = $derived(scheduled instanceof Date && !isNaN(scheduled.getTime()));\n\n\tconst timeZoneOffset = $derived(\n\t\tisValidDate\n\t\t\t? new Intl.DateTimeFormat(language, { timeZone, timeZoneName: 'shortOffset' })\n\t\t\t\t\t.formatToParts(scheduled)\n\t\t\t\t\t.find((part) => part.type === 'timeZoneName')!.value\n\t\t\t: undefined\n\t);\n\tconst isSameAsBrowserTimezone = $derived(() => {\n\t\tif (!isValidDate) return false;\n\t\treturn (\n\t\t\tnew Intl.DateTimeFormat(language, { timeZoneName: 'shortOffset' })\n\t\t\t\t.formatToParts(scheduled)\n\t\t\t\t.find((part) => part.type === 'timeZoneName')!.value == timeZoneOffset\n\t\t);\n\t});\n\n\tfunction weekday(time: Date) {\n\t\tif (variant === 'realtime') {\n\t\t\treturn '';\n\t\t}\n\t\tif (queriedTime === undefined) {\n\t\t\treturn time.toLocaleDateString(language, { timeZone });\n\t\t}\n\t\tconst base = new Date(queriedTime);\n\t\treturn base.toLocaleDateString() === time.toLocaleDateString()\n\t\t\t? ''\n\t\t\t: `(${time.toLocaleString(language, { weekday: 'short', timeZone })})`;\n\t}\n</script>\n\n<div class={cn('text-nowrap flex flex-col', className)} title={timeZoneOffset}>\n\t{#if variant == 'schedule'}\n\t\t<div>\n\t\t\t{formatTime(scheduled, timeZone)}\n\t\t\t{weekday(scheduled)}\n\t\t</div>\n\t\t<div class=\"text-xs font-normal\">{isSameAsBrowserTimezone() ? '' : timeZoneOffset}</div>\n\t{:else if variant === 'realtime-show-always' || (variant === 'realtime' && isRealtime)}\n\t\t<span class:text-destructive={notOnTime} class:text-green-600={roughlyOnTime}>\n\t\t\t{formatTime(t, timeZone)}\n\t\t\t{weekday(t)}\n\t\t</span>\n\t\t{#if variant === 'realtime-show-always' && !isSameAsBrowserTimezone()}\n\t\t\t<div class=\"text-xs font-normal\">\n\t\t\t\t{isSameAsBrowserTimezone() ? '' : timeZoneOffset}\n\t\t\t</div>\n\t\t{/if}\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/TransitModeSelect.svelte",
    "content": "<script lang=\"ts\">\n\timport { t } from '$lib/i18n/translation';\n\timport { BusFront } from '@lucide/svelte';\n\timport * as Select from '$lib/components/ui/select';\n\timport { possibleTransitModes, type TransitMode } from '$lib/Modes';\n\n\tlet {\n\t\ttransitModes = $bindable()\n\t}: {\n\t\ttransitModes: TransitMode[];\n\t} = $props();\n\n\ttype TranslationKey = keyof typeof t;\n\n\tconst availableTransitModes = possibleTransitModes.map((value) => ({\n\t\tvalue,\n\t\tlabel: t[value as TranslationKey] as string\n\t}));\n\n\tconst selectTransitModesLabel = $derived(\n\t\ttransitModes.length == possibleTransitModes.length || transitModes.includes('TRANSIT')\n\t\t\t? t.defaultSelectedModes\n\t\t\t: availableTransitModes\n\t\t\t\t\t.filter((m) => transitModes?.includes(m.value))\n\t\t\t\t\t.map((m) => m.label)\n\t\t\t\t\t.join(', ')\n\t);\n</script>\n\n<Select.Root type=\"multiple\" bind:value={transitModes}>\n\t<Select.Trigger\n\t\tclass=\"flex items-center w-full overflow-hidden\"\n\t\taria-label={t.selectTransitModes}\n\t>\n\t\t<BusFront class=\"mr-[9px] size-6 text-muted-foreground shrink-0\" />\n\t\t{selectTransitModesLabel}\n\t</Select.Trigger>\n\t<Select.Content sideOffset={10}>\n\t\t{#each availableTransitModes as mode, i (i + mode.value)}\n\t\t\t<Select.Item value={mode.value} label={mode.label}>\n\t\t\t\t{mode.label}\n\t\t\t</Select.Item>\n\t\t{/each}\n\t</Select.Content>\n</Select.Root>\n"
  },
  {
    "path": "ui/src/lib/ViaStopOptions.svelte",
    "content": "<script lang=\"ts\">\n\timport { X } from '@lucide/svelte';\n\timport AddressTypeahead from '$lib/AddressTypeahead.svelte';\n\timport { Button } from './components/ui/button';\n\timport NumberSelect from './NumberSelect.svelte';\n\timport { t } from './i18n/translation';\n\timport type { Location } from '$lib/Location';\n\timport { generateTimes } from './generateTimes';\n\timport { formatDurationMin } from './formatDuration';\n\n\tlet {\n\t\tvia = $bindable(),\n\t\tviaMinimumStay = $bindable(),\n\t\tviaLabels = $bindable()\n\t}: {\n\t\tvia: undefined | Location[];\n\t\tviaMinimumStay: undefined | number[];\n\t\tviaLabels: Record<string, string>;\n\t} = $props();\n\n\tconst possibleViaStayDurations = $derived([0, ...generateTimes(2 * 60 * 60)]);\n\tconst viaMinimumStayOptions = $derived(\n\t\tpossibleViaStayDurations.map((duration) => ({\n\t\t\tvalue: (duration / 60).toString(),\n\t\t\tlabel: formatDurationMin(duration / 60)\n\t\t}))\n\t);\n\n\ttype Via = {\n\t\tmatch: Location;\n\t\tstay: number;\n\t};\n\tlet vias = $state<Via[]>(\n\t\tvia?.map(\n\t\t\t(_, i): Via => ({\n\t\t\t\tmatch: via![i],\n\t\t\t\tstay: viaMinimumStay?.[i] ?? 0\n\t\t\t})\n\t\t) ?? []\n\t);\n\tconst add = () => {\n\t\tvias.push({ stay: 0, match: { label: '', match: undefined } });\n\t};\n\tconst remove = (index: number) => {\n\t\tvias = vias.filter((_, i) => i !== index);\n\t};\n\n\t$effect(() => {\n\t\tconst filtered = vias.filter((v) => v.match?.match?.id).map((v) => $state.snapshot(v));\n\n\t\tconst oldVia = $state.snapshot(via);\n\t\tconst nextVia = filtered.length > 0 ? filtered.map((f) => f.match) : undefined;\n\t\tif (JSON.stringify(nextVia) != JSON.stringify(oldVia)) {\n\t\t\tvia = nextVia;\n\t\t}\n\n\t\tconst oldViaMinimumStay = $state.snapshot(viaMinimumStay);\n\t\tconst nextViaMinimumStay = filtered.length > 0 ? filtered.map((f) => f.stay) : undefined;\n\t\tif (JSON.stringify(nextViaMinimumStay) != JSON.stringify(oldViaMinimumStay)) {\n\t\t\tviaMinimumStay = nextViaMinimumStay;\n\t\t}\n\n\t\tconst nextViaLabels: Record<string, string> = {};\n\t\tfiltered.forEach((f, i) => {\n\t\t\tnextViaLabels[`viaLabel${i}`] = f.match.label;\n\t\t});\n\t\tObject.keys(viaLabels).forEach((key) => {\n\t\t\tif (!(key in nextViaLabels)) {\n\t\t\t\tdelete viaLabels[key];\n\t\t\t}\n\t\t});\n\t\tObject.keys(nextViaLabels).forEach((key) => {\n\t\t\tif (viaLabels[key] !== nextViaLabels[key]) {\n\t\t\t\tviaLabels[key] = nextViaLabels[key];\n\t\t\t}\n\t\t});\n\t});\n</script>\n\n<div class=\"space-y-2\">\n\t<div class=\"flex items-center justify-between\">\n\t\t<div class=\"text-sm\">\n\t\t\t{t.viaStops}\n\t\t</div>\n\t\t<Button variant=\"outline\" onclick={add} disabled={vias.length >= 2}>\n\t\t\t{t.addViaStop}\n\t\t</Button>\n\t</div>\n\t<div class=\"space-y-2\">\n\t\t{#each vias as _viaStop, index (index)}\n\t\t\t<div class=\"flex gap-2 items-start\">\n\t\t\t\t<div class=\"grow flex flex-col gap-1\">\n\t\t\t\t\t<div class=\"flex gap-2\">\n\t\t\t\t\t\t<div class=\"grow\">\n\t\t\t\t\t\t\t<AddressTypeahead\n\t\t\t\t\t\t\t\tplaceholder={t.viaStop}\n\t\t\t\t\t\t\t\tname={`via-${index}`}\n\t\t\t\t\t\t\t\tbind:selected={vias[index].match}\n\t\t\t\t\t\t\t\ttype=\"STOP\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"w-24\">\n\t\t\t\t\t\t\t<NumberSelect\n\t\t\t\t\t\t\t\tbind:value={vias[index].stay}\n\t\t\t\t\t\t\t\tpossibleValues={viaMinimumStayOptions}\n\t\t\t\t\t\t\t\tlabelFormatter={formatDurationMin}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tonclick={() => remove(index)}\n\t\t\t\t\taria-label={t.removeViaStop}\n\t\t\t\t>\n\t\t\t\t\t<X class=\"size-4\" />\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/button/button.svelte",
    "content": "<script lang=\"ts\" module>\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAnchorAttributes, HTMLButtonAttributes } from \"svelte/elements\";\n\timport { type VariantProps, tv } from \"tailwind-variants\";\n\n\texport const buttonVariants = tv({\n\t\tbase: \"focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"bg-primary text-primary-foreground hover:bg-primary/90 shadow\",\n\t\t\t\tdestructive:\n\t\t\t\t\t\"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm\",\n\t\t\t\toutline:\n\t\t\t\t\t\"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm\",\n\t\t\t\tsecondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm\",\n\t\t\t\tghost: \"hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\tlink: \"text-primary underline-offset-4 hover:underline\",\n\t\t\t\tchild: \"\",\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: \"px-4 py-2\",\n\t\t\t\tsm: \"h-8 rounded-md px-3 text-xs\",\n\t\t\t\tlg: \"h-10 rounded-md px-8\",\n\t\t\t\ticon: \"h-9 w-9\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t\tsize: \"default\",\n\t\t},\n\t});\n\n\texport type ButtonVariant = VariantProps<typeof buttonVariants>[\"variant\"];\n\texport type ButtonSize = VariantProps<typeof buttonVariants>[\"size\"];\n\n\texport type ButtonProps = WithElementRef<HTMLButtonAttributes> &\n\t\tWithElementRef<HTMLAnchorAttributes> & {\n\t\t\tvariant?: ButtonVariant;\n\t\t\tsize?: ButtonSize;\n\t\t};\n</script>\n\n<script lang=\"ts\">\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tclass: className,\n\t\tvariant = \"default\",\n\t\tsize = \"default\",\n\t\tref = $bindable(null),\n\t\thref = undefined,\n\t\ttype = \"button\",\n\t\tchildren,\n\t\t...restProps\n\t}: ButtonProps = $props();\n</script>\n\n{#if href}\n\t<a\n\t\tbind:this={ref}\n\t\tclass={cn(buttonVariants({ variant, size }), className)}\n\t\t{href}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t</a>\n{:else}\n\t<button\n\t\tbind:this={ref}\n\t\tclass={cn(buttonVariants({ variant, size }), className)}\n\t\t{type}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t</button>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/components/ui/button/index.ts",
    "content": "import Root, {\n\ttype ButtonProps,\n\ttype ButtonSize,\n\ttype ButtonVariant,\n\tbuttonVariants,\n} from \"./button.svelte\";\n\nexport {\n\tRoot,\n\ttype ButtonProps as Props,\n\t//\n\tRoot as Button,\n\tbuttonVariants,\n\ttype ButtonProps,\n\ttype ButtonSize,\n\ttype ButtonVariant,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/card-content.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div bind:this={ref} class={cn(\"p-6\", className)} {...restProps}>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/card-description.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();\n</script>\n\n<p bind:this={ref} class={cn(\"text-muted-foreground text-sm\", className)} {...restProps}>\n\t{@render children?.()}\n</p>\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/card-footer.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div bind:this={ref} class={cn(\"flex items-center p-6 pt-0\", className)} {...restProps}>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/card-header.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div bind:this={ref} class={cn(\"flex flex-col space-y-1.5 p-6 pb-0\", className)} {...restProps}>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/card-title.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tlevel = 3,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {\n\t\tlevel?: 1 | 2 | 3 | 4 | 5 | 6;\n\t} = $props();\n</script>\n\n<div\n\trole=\"heading\"\n\taria-level={level}\n\tbind:this={ref}\n\tclass={cn(\"font-semibold leading-none tracking-tight\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/card.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\timport {onMount } from \"svelte\";\n\timport { restoreScroll} from \"$lib/map/handleScroll\";\n\timport { browser } from \"$app/environment\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n\t\n\tonMount(\n\t\t() => {\n\t\t\tif (!(browser && window.innerWidth < 768) && ref!.classList.contains(\"scrollable\")) {\n\t\t\t\tconst cleanup = restoreScroll(ref!);\n\t\t\t\treturn cleanup;\n\t\t\t}\t\n\t\t}\n\t);\n\n</script>\n\n<div\n\tbind:this={ref}\n\tclass={cn(\"bg-card text-card-foreground rounded-xl border shadow max-w-full\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/card/index.ts",
    "content": "import Root from \"./card.svelte\";\nimport Content from \"./card-content.svelte\";\nimport Description from \"./card-description.svelte\";\nimport Footer from \"./card-footer.svelte\";\nimport Header from \"./card-header.svelte\";\nimport Title from \"./card-title.svelte\";\n\nexport {\n\tRoot,\n\tContent,\n\tDescription,\n\tFooter,\n\tHeader,\n\tTitle,\n\t//\n\tRoot as Card,\n\tContent as CardContent,\n\tDescription as CardDescription,\n\tFooter as CardFooter,\n\tHeader as CardHeader,\n\tTitle as CardTitle,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/dialog-content.svelte",
    "content": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from \"bits-ui\";\n\timport X from \"@lucide/svelte/icons/x\";\n\timport type { Snippet } from \"svelte\";\n\timport * as Dialog from \"./index.js\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tportalProps,\n\t\tchildren,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {\n\t\tportalProps?: DialogPrimitive.PortalProps;\n\t\tchildren: Snippet;\n\t} = $props();\n</script>\n\n<Dialog.Portal {...portalProps}>\n\t<Dialog.Overlay />\n\t<DialogPrimitive.Content\n\t\tbind:ref\n\t\tclass={cn(\n\t\t\t\"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg\",\n\t\t\tclassName\n\t\t)}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t\t<DialogPrimitive.Close\n\t\t\tclass=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none\"\n\t\t>\n\t\t\t<X class=\"size-4\" />\n\t\t\t<span class=\"sr-only\">Close</span>\n\t\t</DialogPrimitive.Close>\n\t</DialogPrimitive.Content>\n</Dialog.Portal>\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/dialog-description.svelte",
    "content": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: DialogPrimitive.DescriptionProps = $props();\n</script>\n\n<DialogPrimitive.Description\n\tbind:ref\n\tclass={cn(\"text-muted-foreground text-sm\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/dialog-footer.svelte",
    "content": "<script lang=\"ts\">\n\timport type { WithElementRef } from \"bits-ui\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tclass={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/dialog-header.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tclass={cn(\"flex flex-col space-y-1.5 text-center sm:text-left\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/dialog-overlay.svelte",
    "content": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: DialogPrimitive.OverlayProps = $props();\n</script>\n\n<DialogPrimitive.Overlay\n\tbind:ref\n\tclass={cn(\n\t\t\"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0  fixed inset-0 z-50 bg-black/80\",\n\t\tclassName\n\t)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/dialog-title.svelte",
    "content": "<script lang=\"ts\">\n\timport { Dialog as DialogPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: DialogPrimitive.TitleProps = $props();\n</script>\n\n<DialogPrimitive.Title\n\tbind:ref\n\tclass={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/dialog/index.ts",
    "content": "import { Dialog as DialogPrimitive } from \"bits-ui\";\n\nimport Title from \"./dialog-title.svelte\";\nimport Footer from \"./dialog-footer.svelte\";\nimport Header from \"./dialog-header.svelte\";\nimport Overlay from \"./dialog-overlay.svelte\";\nimport Content from \"./dialog-content.svelte\";\nimport Description from \"./dialog-description.svelte\";\n\nconst Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;\nconst Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;\nconst Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;\nconst Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;\n\nexport {\n\tRoot,\n\tTitle,\n\tPortal,\n\tFooter,\n\tHeader,\n\tTrigger,\n\tOverlay,\n\tContent,\n\tDescription,\n\tClose,\n\t//\n\tRoot as Dialog,\n\tTitle as DialogTitle,\n\tPortal as DialogPortal,\n\tFooter as DialogFooter,\n\tHeader as DialogHeader,\n\tTrigger as DialogTrigger,\n\tOverlay as DialogOverlay,\n\tContent as DialogContent,\n\tDescription as DialogDescription,\n\tClose as DialogClose,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/label/index.ts",
    "content": "import Root from \"./label.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as Label,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/label/label.svelte",
    "content": "<script lang=\"ts\">\n\timport { Label as LabelPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: LabelPrimitive.RootProps = $props();\n</script>\n\n<LabelPrimitive.Root\n\tbind:ref\n\tclass={cn(\n\t\t\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n\t\tclassName\n\t)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/radio-group/index.ts",
    "content": "import Root from \"./radio-group.svelte\";\nimport Item from \"./radio-group-item.svelte\";\n\nexport {\n\tRoot,\n\tItem,\n\t//\n\tRoot as RadioGroup,\n\tItem as RadioGroupItem,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/radio-group/radio-group-item.svelte",
    "content": "<script lang=\"ts\">\n\timport { RadioGroup as RadioGroupPrimitive, type WithoutChildrenOrChild } from 'bits-ui';\n\timport { Circle } from '@lucide/svelte';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> & {\n\t\tvalue: string;\n\t} = $props();\n</script>\n\n<RadioGroupPrimitive.Item\n\tbind:ref\n\tclass={cn(\n\t\t'border-primary text-primary focus-visible:ring-ring aspect-square size-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{#snippet children({ checked }: { checked: boolean })}\n\t\t<div class=\"flex items-center justify-center\">\n\t\t\t{#if checked}\n\t\t\t\t<Circle class=\"fill-primary size-3.5\" />\n\t\t\t{/if}\n\t\t</div>\n\t{/snippet}\n</RadioGroupPrimitive.Item>\n"
  },
  {
    "path": "ui/src/lib/components/ui/radio-group/radio-group.svelte",
    "content": "<script lang=\"ts\">\n\timport { RadioGroup as RadioGroupPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tvalue = $bindable(),\n\t\t...restProps\n\t}: RadioGroupPrimitive.RootProps = $props();\n\n\texport { className as class };\n</script>\n\n<RadioGroupPrimitive.Root bind:value class={cn(\"grid gap-1\", className)} {...restProps} bind:ref />\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/index.ts",
    "content": "import { Select as SelectPrimitive } from \"bits-ui\";\n\nimport GroupHeading from \"./select-group-heading.svelte\";\nimport Item from \"./select-item.svelte\";\nimport Content from \"./select-content.svelte\";\nimport Trigger from \"./select-trigger.svelte\";\nimport Separator from \"./select-separator.svelte\";\nimport ScrollDownButton from \"./select-scroll-down-button.svelte\";\nimport ScrollUpButton from \"./select-scroll-up-button.svelte\";\n\nconst Root = SelectPrimitive.Root;\nconst Group = SelectPrimitive.Group;\n\nexport {\n\tRoot,\n\tItem,\n\tGroup,\n\tGroupHeading,\n\tContent,\n\tTrigger,\n\tSeparator,\n\tScrollDownButton,\n\tScrollUpButton,\n\t//\n\tRoot as Select,\n\tItem as SelectItem,\n\tGroup as SelectGroup,\n\tGroupHeading as SelectGroupHeading,\n\tContent as SelectContent,\n\tTrigger as SelectTrigger,\n\tSeparator as SelectSeparator,\n\tScrollDownButton as SelectScrollDownButton,\n\tScrollUpButton as SelectScrollUpButton,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-content.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive, type WithoutChild } from \"bits-ui\";\n\timport * as Select from \"./index.js\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tsideOffset = 4,\n\t\tportalProps,\n\t\tchildren,\n\t\t...restProps\n\t}: WithoutChild<SelectPrimitive.ContentProps> & {\n\t\tportalProps?: SelectPrimitive.PortalProps;\n\t} = $props();\n</script>\n\n<SelectPrimitive.Portal {...portalProps}>\n\t<SelectPrimitive.Content\n\t\tbind:ref\n\t\t{sideOffset}\n\t\tclass={cn(\n\t\t\t\"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n\t\t\tclassName\n\t\t)}\n\t\t{...restProps}\n\t>\n\t\t<Select.ScrollUpButton />\n\t\t<SelectPrimitive.Viewport\n\t\t\tclass={cn(\n\t\t\t\t\"h-[var(--bits-select-anchor-height)] w-full min-w-[var(--bits-select-anchor-width)] p-1\"\n\t\t\t)}\n\t\t>\n\t\t\t{@render children?.()}\n\t\t</SelectPrimitive.Viewport>\n\t\t<Select.ScrollDownButton />\n\t</SelectPrimitive.Content>\n</SelectPrimitive.Portal>\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-group-heading.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: SelectPrimitive.GroupHeadingProps = $props();\n</script>\n\n<SelectPrimitive.GroupHeading\n\tbind:ref\n\tclass={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-item.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';\n\timport { Check } from '@lucide/svelte';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tvalue,\n\t\tlabel,\n\t\tchildren: childrenProp,\n\t\t...restProps\n\t}: WithoutChild<SelectPrimitive.ItemProps> = $props();\n</script>\n\n<SelectPrimitive.Item\n\tbind:ref\n\t{value}\n\tclass={cn(\n\t\t'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{#snippet children({ selected, highlighted }: { selected: boolean; highlighted: boolean })}\n\t\t<span class=\"absolute right-2 flex size-3.5 items-center justify-center\">\n\t\t\t{#if selected}\n\t\t\t\t<Check class=\"size-4\" />\n\t\t\t{/if}\n\t\t</span>\n\t\t{#if childrenProp}\n\t\t\t{@render childrenProp({ selected, highlighted })}\n\t\t{:else}\n\t\t\t{label || value}\n\t\t{/if}\n\t{/snippet}\n</SelectPrimitive.Item>\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-scroll-down-button.svelte",
    "content": "<script lang=\"ts\">\n\timport { ChevronDown } from '@lucide/svelte';\n\timport { Select as SelectPrimitive, type WithoutChildrenOrChild } from 'bits-ui';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();\n</script>\n\n<SelectPrimitive.ScrollDownButton\n\tbind:ref\n\tclass={cn('flex cursor-default items-center justify-center py-1', className)}\n\t{...restProps}\n>\n\t<ChevronDown class=\"size-4\" />\n</SelectPrimitive.ScrollDownButton>\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-scroll-up-button.svelte",
    "content": "<script lang=\"ts\">\n\timport { ChevronUp } from '@lucide/svelte';\n\timport { Select as SelectPrimitive, type WithoutChildrenOrChild } from 'bits-ui';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();\n</script>\n\n<SelectPrimitive.ScrollUpButton\n\tbind:ref\n\tclass={cn('flex cursor-default items-center justify-center py-1', className)}\n\t{...restProps}\n>\n\t<ChevronUp class=\"size-4\" />\n</SelectPrimitive.ScrollUpButton>\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-separator.svelte",
    "content": "<script lang=\"ts\">\n\timport type { Separator as SeparatorPrimitive } from \"bits-ui\";\n\timport { Separator } from \"$lib/components/ui/separator/index.js\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: SeparatorPrimitive.RootProps = $props();\n</script>\n\n<Separator bind:ref class={cn(\"bg-muted -mx-1 my-1 h-px\", className)} {...restProps} />\n"
  },
  {
    "path": "ui/src/lib/components/ui/select/select-trigger.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';\n\timport { ChevronDown } from '@lucide/svelte';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithoutChild<SelectPrimitive.TriggerProps> = $props();\n</script>\n\n<SelectPrimitive.Trigger\n\tbind:ref\n\tclass={cn(\n\t\t'border-input ring-offset-background data-[placeholder]:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n\t<ChevronDown class=\"size-4 opacity-50\" />\n</SelectPrimitive.Trigger>\n"
  },
  {
    "path": "ui/src/lib/components/ui/separator/index.ts",
    "content": "import Root from \"./separator.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as Separator,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/separator/separator.svelte",
    "content": "<script lang=\"ts\">\n\timport { Separator as SeparatorPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\torientation = \"horizontal\",\n\t\t...restProps\n\t}: SeparatorPrimitive.RootProps = $props();\n</script>\n\n<SeparatorPrimitive.Root\n\tbind:ref\n\tclass={cn(\n\t\t\"bg-border shrink-0\",\n\t\torientation === \"horizontal\" ? \"h-[1px] w-full\" : \"min-h-full w-[1px]\",\n\t\tclassName\n\t)}\n\t{orientation}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/switch/index.ts",
    "content": "import Root from \"./switch.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as Switch,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/switch/switch.svelte",
    "content": "<script lang=\"ts\">\n\timport { Label, Switch } from 'bits-ui';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tchecked = $bindable(false),\n\t\tdisabled,\n\t\tlabel,\n\t\tid,\n\t\tclass: className,\n\t\tonCheckedChange: onCheckedChange\n\t}: {\n\t\tchecked: boolean;\n\t\tdisabled?: boolean;\n\t\tlabel: string;\n\t\tid: string;\n\t\tclass?: string;\n\t\tonCheckedChange?: (checked: boolean) => void;\n\t} = $props();\n</script>\n\n<div class=\"flex items-center space-x-3 my-2\">\n\t<Switch.Root\n\t\t{id}\n\t\t{disabled}\n\t\tbind:checked\n\t\tclass={cn(\n\t\t\t'peer inline-flex h-[21px] min-h-[21px] w-[45px] shrink-0 cursor-pointer items-center rounded-full px-[3px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-border data-[state=unchecked]:shadow-mini-inset dark:data-[state=checked]:bg-foreground',\n\t\t\tclassName\n\t\t)}\n\t\tonCheckedChange={onCheckedChange}\n\t>\n\t\t<Switch.Thumb\n\t\t\tclass=\"pointer-events-none block size-[15px] shrink-0 rounded-full bg-background transition-transform data-[state=checked]:translate-x-6 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini dark:border dark:border-background/30 dark:bg-foreground dark:shadow-popover dark:data-[state=unchecked]:border\"\n\t\t/>\n\t</Switch.Root>\n\t<Label.Root for={id} class=\"text-sm font-medium\">{label}</Label.Root>\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/index.ts",
    "content": "import Root from \"./table.svelte\";\nimport Body from \"./table-body.svelte\";\nimport Caption from \"./table-caption.svelte\";\nimport Cell from \"./table-cell.svelte\";\nimport Footer from \"./table-footer.svelte\";\nimport Head from \"./table-head.svelte\";\nimport Header from \"./table-header.svelte\";\nimport Row from \"./table-row.svelte\";\n\nexport {\n\tRoot,\n\tBody,\n\tCaption,\n\tCell,\n\tFooter,\n\tHead,\n\tHeader,\n\tRow,\n\t//\n\tRoot as Table,\n\tBody as TableBody,\n\tCaption as TableCaption,\n\tCell as TableCell,\n\tFooter as TableFooter,\n\tHead as TableHead,\n\tHeader as TableHeader,\n\tRow as TableRow,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-body.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();\n</script>\n\n<tbody bind:this={ref} class={cn(\"[&_tr:last-child]:border-0\", className)} {...restProps}>\n\t{@render children?.()}\n</tbody>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-caption.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();\n</script>\n\n<caption bind:this={ref} class={cn(\"text-muted-foreground mt-4 text-sm\", className)} {...restProps}>\n\t{@render children?.()}\n</caption>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-cell.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLTdAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLTdAttributes> = $props();\n</script>\n\n<td\n\tbind:this={ref}\n\tclass={cn(\n\t\t\"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</td>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-footer.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();\n</script>\n\n<tfoot bind:this={ref} class={cn(\"bg-muted/50 font-medium\", className)} {...restProps}>\n\t{@render children?.()}\n</tfoot>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-head.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLThAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLThAttributes> = $props();\n</script>\n\n<th\n\tbind:this={ref}\n\tclass={cn(\n\t\t\"text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</th>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-header.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();\n</script>\n\n<thead bind:this={ref} class={cn(\"[&_tr]:border-b\", className)} {...restProps}>\n\t{@render children?.()}\n</thead>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table-row.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();\n</script>\n\n<tr\n\tbind:this={ref}\n\tclass={cn(\n\t\t\"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</tr>\n"
  },
  {
    "path": "ui/src/lib/components/ui/table/table.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLTableAttributes } from \"svelte/elements\";\n\timport type { WithElementRef } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLTableAttributes> = $props();\n</script>\n\n<div class=\"relative w-full overflow-auto\">\n\t<table bind:this={ref} class={cn(\"w-full caption-bottom text-sm\", className)} {...restProps}>\n\t\t{@render children?.()}\n\t</table>\n</div>\n"
  },
  {
    "path": "ui/src/lib/components/ui/tabs/index.ts",
    "content": "import { Tabs as TabsPrimitive } from \"bits-ui\";\nimport Content from \"./tabs-content.svelte\";\nimport List from \"./tabs-list.svelte\";\nimport Trigger from \"./tabs-trigger.svelte\";\n\nconst Root = TabsPrimitive.Root;\n\nexport {\n\tRoot,\n\tContent,\n\tList,\n\tTrigger,\n\t//\n\tRoot as Tabs,\n\tContent as TabsContent,\n\tList as TabsList,\n\tTrigger as TabsTrigger,\n};"
  },
  {
    "path": "ui/src/lib/components/ui/tabs/tabs-content.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$Props = TabsPrimitive.ContentProps;\n\n\tlet className: $$Props[\"class\"] = undefined;\n\texport let value: $$Props[\"value\"];\n\texport { className as class };\n</script>\n\n<TabsPrimitive.Content\n\tclass={cn(\n\t\t\"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n\t\tclassName\n\t)}\n\t{value}\n\t{...$$restProps}\n>\n\t<slot />\n</TabsPrimitive.Content>"
  },
  {
    "path": "ui/src/lib/components/ui/tabs/tabs-list.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\ttype $$Props = TabsPrimitive.ListProps;\n\n\tlet className: $$Props[\"class\"] = undefined;\n\texport { className as class };\n</script>\n\n<TabsPrimitive.List\n\tclass={cn(\n\t\t\"bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1\",\n\t\tclassName\n\t)}\n\t{...$$restProps}\n>\n\t<slot />\n</TabsPrimitive.List>"
  },
  {
    "path": "ui/src/lib/components/ui/tabs/tabs-trigger.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from 'bits-ui';\n\timport { cn } from '$lib/utils.js';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tvalue = $bindable(),\n\t\t...restProps\n\t}: TabsPrimitive.TriggerProps = $props();\n\n\texport { className as class };\n</script>\n\n<TabsPrimitive.Trigger\n\tclass={cn(\n\t\t'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',\n\t\tclassName\n\t)}\n\t{value}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/components/ui/tabs/tabs.svelte",
    "content": "<script lang=\"ts\" module>\n\timport { tv } from 'tailwind-variants';\n\n\texport const tabVariants = tv({\n\t\tbase: 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50',\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'bg-transparent',\n\t\t\t\toutline:\n\t\t\t\t\t'border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-sm'\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: 'h-9 min-w-9 px-3',\n\t\t\t\tsm: 'h-8 min-w-8 px-2',\n\t\t\t\tlg: 'h-10 min-w-10 px-3'\n\t\t\t}\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t\tsize: 'default'\n\t\t}\n\t});\n</script>\n\n<script lang=\"ts\">\n</script>\n"
  },
  {
    "path": "ui/src/lib/components/ui/toggle/index.ts",
    "content": "import Root from \"./toggle.svelte\";\nexport {\n\ttoggleVariants,\n\ttype ToggleSize,\n\ttype ToggleVariant,\n\ttype ToggleVariants,\n} from \"./toggle.svelte\";\n\nexport {\n\tRoot,\n\t//\n\tRoot as Toggle,\n};\n"
  },
  {
    "path": "ui/src/lib/components/ui/toggle/toggle.svelte",
    "content": "<script lang=\"ts\" module>\n\timport { type VariantProps, tv } from \"tailwind-variants\";\n\n\texport const toggleVariants = tv({\n\t\tbase: \"hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-blue-600 inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"bg-transparent\",\n\t\t\t\toutline:\n\t\t\t\t\t\"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-sm\",\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: \"h-9 min-w-9 px-3\",\n\t\t\t\tsm: \"h-8 min-w-8 px-2\",\n\t\t\t\tlg: \"h-10 min-w-10 px-3\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t\tsize: \"default\",\n\t\t},\n\t});\n\n\texport type ToggleVariant = VariantProps<typeof toggleVariants>[\"variant\"];\n\texport type ToggleSize = VariantProps<typeof toggleVariants>[\"size\"];\n\texport type ToggleVariants = VariantProps<typeof toggleVariants>;\n</script>\n\n<script lang=\"ts\">\n\timport { Toggle as TogglePrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tpressed = $bindable(false),\n\t\tclass: className,\n\t\tsize = \"default\",\n\t\tvariant = \"default\",\n\t\t...restProps\n\t}: TogglePrimitive.RootProps & {\n\t\tvariant?: ToggleVariant;\n\t\tsize?: ToggleSize;\n\t} = $props();\n</script>\n\n<TogglePrimitive.Root\n\tbind:ref\n\tbind:pressed\n\tclass={cn(toggleVariants({ variant, size }), className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "ui/src/lib/constants.ts",
    "content": "export const LEVEL_MIN_ZOOM = 17;\n"
  },
  {
    "path": "ui/src/lib/defaults.ts",
    "content": "import type { PlanData } from '@motis-project/motis-client';\n\nexport const defaultQuery = {\n\ttime: undefined,\n\tfromPlace: undefined,\n\ttoPlace: undefined,\n\tvia: undefined,\n\tviaMinimumStay: undefined,\n\tarriveBy: false,\n\ttimetableView: true,\n\twithFares: false,\n\tsearchWindow: 900,\n\tpedestrianProfile: 'FOOT',\n\ttransitModes: ['TRANSIT'],\n\tpreTransitModes: ['WALK'],\n\tpostTransitModes: ['WALK'],\n\tdirectModes: ['WALK'],\n\tpreTransitRentalFormFactors: [],\n\tpostTransitRentalFormFactors: [],\n\tdirectRentalFormFactors: [],\n\tpreTransitRentalProviderGroups: [],\n\tpostTransitRentalProviderGroups: [],\n\tdirectRentalProviderGroups: [],\n\tpreTransitRentalPropulsionTypes: [],\n\tpostTransitRentalPropulsionTypes: [],\n\tdirectRentalPropulsionTypes: [],\n\tignorePreTransitRentalReturnConstraints: false,\n\tignorePostTransitRentalReturnConstraints: false,\n\tignoreDirectRentalReturnConstraints: false,\n\trequireBikeTransport: false,\n\trequireCarTransport: false,\n\televationCosts: 'NONE',\n\tuseRoutedTransfers: false,\n\tjoinInterlinedLegs: true,\n\tmaxMatchingDistance: 25,\n\tmaxTransfers: 14,\n\tmaxTravelTime: 30 * 60,\n\tmaxPreTransitTime: 900,\n\tmaxPostTransitTime: 900,\n\tmaxDirectTime: 1800,\n\tfastestDirectFactor: 1.0,\n\tadditionalTransferTime: 0,\n\ttransferTimeFactor: 1,\n\tnumItineraries: 5,\n\tcircleResolution: undefined,\n\tmaxItineraries: undefined,\n\tpassengers: 1,\n\tluggage: 0,\n\tslowDirect: false,\n\tisochronesDisplayLevel: 'GEOMETRY_CIRCLES',\n\tisochronesColor: '#ffff00',\n\tisochronesOpacity: 250,\n\talgorithm: 'PONG'\n};\n\nexport const omitDefaults = (query: PlanData['query']): PlanData['query'] => {\n\tconst queryCopy: PlanData['query'] = { ...query };\n\tObject.keys(queryCopy).forEach((key) => {\n\t\tif (key in defaultQuery) {\n\t\t\tconst value = queryCopy[key as keyof PlanData['query']];\n\t\t\tconst defaultValue = defaultQuery[key as keyof typeof defaultQuery];\n\t\t\tif (JSON.stringify(value) === JSON.stringify(defaultValue)) {\n\t\t\t\tdelete queryCopy[key as keyof PlanData['query']];\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.warn(`Unknown query parameter: ${key}`);\n\t\t}\n\t});\n\treturn queryCopy;\n};\n"
  },
  {
    "path": "ui/src/lib/formatDuration.ts",
    "content": "export const formatDurationMin = (t: number): string => {\n\tlet hours = Math.floor(t / 60);\n\tlet minutes = Math.ceil(t - hours * 60);\n\tif (minutes === 60) {\n\t\thours += 1;\n\t\tminutes = 0;\n\t}\n\tconst str = [\n\t\thours !== 0 ? hours + ' h' : '',\n\t\tminutes !== 0 || hours === 0 ? minutes + ' min' : ''\n\t]\n\t\t.join(' ')\n\t\t.trim();\n\treturn str;\n};\n\nexport const formatDurationSec = (t: number): string => {\n\tlet hours = Math.floor(t / 3600);\n\tlet minutes = Math.ceil((t - hours * 3600) / 60);\n\tif (minutes === 60) {\n\t\thours += 1;\n\t\tminutes = 0;\n\t}\n\tconst str = [\n\t\thours !== 0 ? hours + ' h' : '',\n\t\tminutes !== 0 || hours === 0 ? minutes + ' min' : ''\n\t]\n\t\t.join(' ')\n\t\t.trim();\n\treturn str;\n};\n\nexport const formatDistanceMeters = (m: number | undefined): string => {\n\tif (!m) return '';\n\tconst kilometers = Math.floor(m / 1000);\n\tconst meters = kilometers > 5 ? 0 : Math.ceil(m - kilometers * 1000);\n\tconst str = [kilometers !== 0 ? kilometers + ' km' : '', meters !== 0 ? meters + ' m' : '']\n\t\t.join(' ')\n\t\t.trim();\n\treturn str;\n};\n"
  },
  {
    "path": "ui/src/lib/generateTimes.ts",
    "content": "export const generateTimes = (limit: number): number[] => {\n\tconst times: number[] = [];\n\tlet t = 1;\n\twhile (t <= limit / 60) {\n\t\ttimes.push(t * 60);\n\t\tif (t < 5) {\n\t\t\tt += 4;\n\t\t} else if (t < 30) {\n\t\t\tt += 5;\n\t\t} else if (t < 60) {\n\t\t\tt += 10;\n\t\t} else if (t < 120) {\n\t\t\tt += 30;\n\t\t} else {\n\t\t\tt += 60;\n\t\t}\n\t}\n\tif (times[times.length - 1] !== limit) {\n\t\ttimes.push(limit);\n\t}\n\n\treturn times;\n};\n"
  },
  {
    "path": "ui/src/lib/getModeName.ts",
    "content": "import type { Leg } from '@motis-project/motis-client';\n\nimport { t } from './i18n/translation';\n\nexport const getModeName = (l: Leg) => {\n\tswitch (l.mode) {\n\t\tcase 'WALK':\n\t\t\treturn t.walk;\n\t\tcase 'BIKE':\n\t\t\treturn t.bike;\n\t\tcase 'RENTAL':\n\t\t\tswitch (l.rental?.formFactor) {\n\t\t\t\tcase 'BICYCLE':\n\t\t\t\t\treturn t.bike;\n\t\t\t\tcase 'CARGO_BICYCLE':\n\t\t\t\t\treturn t.cargoBike;\n\t\t\t\tcase 'CAR':\n\t\t\t\t\treturn t.car;\n\t\t\t\tcase 'MOPED':\n\t\t\t\t\treturn t.moped;\n\t\t\t\tcase 'SCOOTER_SEATED':\n\t\t\t\t\treturn t.scooterSeated;\n\t\t\t\tcase 'SCOOTER_STANDING':\n\t\t\t\t\treturn t.scooterStanding;\n\t\t\t\tcase 'OTHER':\n\t\t\t\tdefault:\n\t\t\t\t\treturn t.unknownVehicleType;\n\t\t\t}\n\t\tcase 'CAR':\n\t\tcase 'CAR_PARKING':\n\t\t\treturn t.car;\n\t\tcase 'ODM':\n\t\t\treturn t.taxi;\n\t\tcase 'FLEX':\n\t\t\treturn 'Flex';\n\t\tdefault:\n\t\t\treturn `${l.mode}`;\n\t}\n};\n"
  },
  {
    "path": "ui/src/lib/i18n/bg.ts",
    "content": "import type { Translations } from './translation';\n\nconst translations: Translations = {\n\tticket: 'Билет',\n\tticketOptions: 'Опции за билет',\n\tincludedInTicket: 'Включено в билета',\n\tjourneyDetails: 'Детайли за пътуването',\n\ttransfers: 'прекачвания',\n\twalk: 'Пеша',\n\tbike: 'Велосипед',\n\tcargoBike: 'Товарен велосипед',\n\tscooterStanding: 'тротинетка',\n\tscooterSeated: 'тротинетка',\n\tcar: 'Автомобил',\n\ttaxi: 'Такси',\n\tmoped: 'Скутер',\n\tunknownVehicleType: 'Неизвестен тип превозно средство',\n\telectricAssist: 'Електрическа помощ',\n\telectric: 'Електрически',\n\tcombustion: 'Двигател с вътрешно горене',\n\tcombustionDiesel: 'Дизел',\n\thybrid: 'Хибрид',\n\tplugInHybrid: 'Зареждаем хибрид',\n\thydrogenFuelCell: 'Водородна горивна клетка',\n\tfrom: 'От',\n\tto: 'До',\n\tviaStop: 'Междинна спирка',\n\tviaStops: 'Междинни спирки',\n\taddViaStop: 'Добави междинна спирка',\n\tremoveViaStop: 'Премахни междинната спирка',\n\tviaStayDuration: 'Минимален престой',\n\tposition: 'местоположение',\n\tarrival: 'Пристигане',\n\tdeparture: 'Заминаване',\n\tduration: 'Продължителност',\n\tarrivals: 'Пристигания',\n\tlater: 'по-късно',\n\tearlier: 'по-рано',\n\tconnections: 'Връзки',\n\tdepartures: 'Заминавания',\n\tswitchToArrivals: 'Превключи към пристигания',\n\tswitchToDepartures: 'Превключи към заминавания',\n\ttrack: 'Коловоз',\n\tplatform: 'Перон',\n\ttrackAbr: 'Кол.',\n\tplatformAbr: 'Перон',\n\tarrivalOnTrack: 'Пристигане на коловоз',\n\ttripIntermediateStops: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Без междинни спирки';\n\t\t\tcase 1:\n\t\t\t\treturn '1 междинна спирка';\n\t\t\tdefault:\n\t\t\t\treturn `${n} междинни спирки`;\n\t\t}\n\t},\n\tsharingProvider: 'Оператор',\n\tsharingProviders: 'Оператори',\n\treturnOnlyAtStations: 'Превозното средство трябва да се върне на станция.',\n\troundtripStationReturnConstraint: 'Превозното средство трябва да се върне на началната станция.',\n\trentalStation: 'Станция',\n\trentalGeofencingZone: 'Зона',\n\tnoItinerariesFound: 'Не са намерени маршрути.',\n\tadvancedSearchOptions: 'Разширени настройки',\n\tselectTransitModes: 'Изберете видове транспорт',\n\tdefaultSelectedModes: 'Всички видове транспорт',\n\tdefaultSelectedProviders: 'Всички превозвачи',\n\tselectElevationCosts: 'Избягвай стръмни наклони.',\n\twheelchair: 'инвалидна количка',\n\tuseRoutedTransfers: 'Използвай посочените прекачвания',\n\tbikeRental: 'наемане на велосипед',\n\trequireBikeTransport: 'Превоз на велосипед',\n\trequireCarTransport: 'Превоз на автомобил',\n\tconsiderRentalReturnConstraints: 'Върни наетите превозни средства в рамките на пътуването',\n\tdefault: 'По подразбиране',\n\ttimetableSources: 'Източници на разписания',\n\ttripCancelled: 'Пътуването е отменено',\n\tstopCancelled: 'Спирката е отменена',\n\tinOutDisallowed: 'Качване/слизане не е позволено',\n\tinDisallowed: 'Качването не е позволено',\n\toutDisallowed: 'Слизането не е позволено',\n\tunscheduledTrip: 'извънреден курс',\n\talertsAvailable: 'налични известия',\n\tdataExpiredSince: 'Данните са остарели, последно валидни на',\n\tFLEX: 'По заявка',\n\tWALK: 'Пеша',\n\tBIKE: 'Велосипед',\n\tRENTAL: 'Нает',\n\tRIDE_SHARING: 'Споделено пътуване',\n\tCAR: 'Автомобил',\n\tCAR_DROPOFF: 'Слизане от автомобила',\n\tCAR_PARKING: 'Паркинг (P+R)',\n\tTRANSIT: 'Градски транспорт',\n\tTRAM: 'Трамвай',\n\tSUBWAY: 'Метро',\n\tFERRY: 'Ферибот',\n\tAIRPLANE: 'Самолет',\n\tSUBURBAN: 'Градска железница',\n\tBUS: 'Автобус',\n\tCOACH: 'Междуградски автобус',\n\tRAIL: 'Влак',\n\tHIGHSPEED_RAIL: 'Високоскоростен влак',\n\tLONG_DISTANCE: 'Междуградски влак',\n\tNIGHT_RAIL: 'Нощен влак',\n\tREGIONAL_FAST_RAIL: 'Бърз регионален влак',\n\tODM: 'По заявка',\n\tREGIONAL_RAIL: 'Регионален влак',\n\tOTHER: 'Други',\n\troutingSegments: {\n\t\tmaxTransfers: 'Макс. прекачвания',\n\t\tmaxTravelTime: 'Макс. време за пътуване',\n\t\tfirstMile: 'Първи километър',\n\t\tlastMile: 'Последен километър',\n\t\tdirect: 'Директна връзка',\n\t\tmaxPreTransitTime: 'Максимално време преди транзит',\n\t\tmaxPostTransitTime: 'Максимално време след транзит',\n\t\tmaxDirectTime: 'Максимално време без транзит'\n\t},\n\televationCosts: {\n\t\tNONE: 'Без наклон',\n\t\tLOW: 'Лек наклон',\n\t\tHIGH: 'Голям наклон'\n\t},\n\tisochrones: {\n\t\ttitle: 'Достъпен периметър',\n\t\tdisplayLevel: 'Ниво на показване',\n\t\tmaxComputeLevel: 'Макс. ниво на изчисление',\n\t\tcanvasRects: 'Правоъгълници (слой)',\n\t\tcanvasCircles: 'Кръгове (слой)',\n\t\tgeojsonCircles: 'Кръгове (геометрия)',\n\t\tstyling: 'Стил',\n\t\tnoData: 'Няма данни',\n\t\trequestFailed: 'Заявката е неуспешна'\n\t},\n\talerts: {\n\t\tvalidFrom: 'Валидно от',\n\t\tuntil: 'до',\n\t\tinformation: 'Информация',\n\t\tmore: 'още'\n\t},\n\tRENTAL_BICYCLE: 'Споделен велосипед',\n\tRENTAL_CARGO_BICYCLE: 'Споделен товарен велосипед',\n\tRENTAL_CAR: 'Нает автомобил',\n\tRENTAL_MOPED: 'Споделен скутер',\n\tRENTAL_SCOOTER_STANDING: 'Споделена тротинетка',\n\tRENTAL_SCOOTER_SEATED: 'Споделена седяща тротинетка',\n\tRENTAL_OTHER: 'Друго споделено превозно средство',\n\tincline: 'Наклон',\n\tCABLE_CAR: 'Въжен лифт',\n\tFUNICULAR: 'Фуникулер',\n\tAERIAL_LIFT: 'Въздушен лифт',\n\ttoll: 'Внимание! Платен път.',\n\taccessRestriction: 'забранен достъп',\n\tcontinuesAs: 'Продължава като',\n\trent: 'Наем',\n\tcopyToClipboard: 'Копирай в клипборда',\n\trideThroughAllowed: 'Минаване е позволено',\n\trideThroughNotAllowed: 'Минаване не е позволено',\n\trideEndAllowed: 'Паркиране е позволено',\n\trideEndNotAllowed: 'Паркиране не е позволено',\n\tDEBUG_BUS_ROUTE: 'Маршрут на автобус (Отстраняване на грешки)',\n\tDEBUG_RAILWAY_ROUTE: 'Маршрут на влак (Отстраняване на грешки)',\n\tDEBUG_FERRY_ROUTE: 'Маршрут на ферибот (Отстраняване на грешки)',\n\troutes: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Без маршрут';\n\t\t\tcase 1:\n\t\t\t\treturn '1 маршрут';\n\t\t\tdefault:\n\t\t\t\treturn `${n} маршрута`;\n\t\t}\n\t}\n};\n\nexport default translations;\n"
  },
  {
    "path": "ui/src/lib/i18n/cs.ts",
    "content": "import type { Translations } from './translation';\n\nconst translations: Translations = {\n\tticket: 'Jízdenka',\n\tticketOptions: 'Možnosti jízdenky',\n\tincludedInTicket: 'Zahrnuté v jízdence',\n\tjourneyDetails: 'Detail cesty',\n\ttransfers: 'přestupy',\n\twalk: 'Pěšky',\n\tbike: 'Kolo',\n\tcargoBike: 'Nákladní kolo',\n\tscooterStanding: 'Koloběžka',\n\tscooterSeated: 'Koloběžka se sedačkou',\n\tcar: 'Auto',\n\ttaxi: 'Taxi',\n\tmoped: 'Skútr',\n\tunknownVehicleType: 'Neznámý typ vozidla',\n\telectricAssist: 'Elektrická podpora',\n\telectric: 'Elektrické',\n\tcombustion: 'Spalovací',\n\tcombustionDiesel: 'Diesel',\n\thybrid: 'Hybridní',\n\tplugInHybrid: 'Plug-in hybrid',\n\thydrogenFuelCell: 'Vodíkový palivový článek',\n\tfrom: 'Z',\n\tto: 'Do',\n\tviaStop: 'Mezizastávka',\n\tviaStops: 'Mezizastávky',\n\taddViaStop: 'Přidat mezizastávku',\n\tremoveViaStop: 'Odebrat mezizastávku',\n\tviaStayDuration: 'Minimální pobyt',\n\tposition: 'Pozice',\n\tarrival: 'Příjezd',\n\tdeparture: 'Odjezd',\n\tduration: 'Čas cesty',\n\tarrivals: 'Příjezdy',\n\tlater: 'později',\n\tearlier: 'dřive',\n\tdepartures: 'Odjezdy',\n\tconnections: 'Spoje',\n\tswitchToArrivals: 'Přepni na příjezdy',\n\tswitchToDepartures: 'Přepni na odjezdy',\n\ttrack: 'Kolej',\n\tplatform: 'Nástupiště',\n\tplatformAbr: 'Nást.',\n\ttrackAbr: 'K.',\n\tarrivalOnTrack: 'Příjezd na kolej',\n\ttripIntermediateStops: (n: number) => {\n\t\tif (n == 0) {\n\t\t\treturn 'Bez mezizastávek';\n\t\t}\n\t\tif (n == 1) {\n\t\t\treturn '1 mezizastávka';\n\t\t}\n\t\tif (n % 10 > 1 && n % 10 < 5 && n != 12 && n != 13 && n != 14) {\n\t\t\treturn `${n} mezizastávky`;\n\t\t}\n\t\treturn `${n} mezizastávek`;\n\t},\n\tsharingProvider: 'Poskytovatel dat',\n\tsharingProviders: 'Poskytovatelé dat',\n\treturnOnlyAtStations: 'Vozidlo musí být vráceno na stanici.',\n\troundtripStationReturnConstraint: 'Pojezd musí být vrácen k počáteční stanice',\n\trentalStation: 'Stanice',\n\trentalGeofencingZone: 'Zóna',\n\tnoItinerariesFound: 'Spojení nebylo nalezeno.',\n\tadvancedSearchOptions: 'Možnosti',\n\tselectTransitModes: 'Vyber dopravní prostředky',\n\tdefaultSelectedModes: 'Všechny dopravní prostředky',\n\tdefaultSelectedProviders: 'Všichni poskytovatelé',\n\tselectElevationCosts: 'Bez prudkého stoupání.',\n\twheelchair: 'Bezbariérové přestupy',\n\tuseRoutedTransfers: 'Počítej trasu pro přestupy',\n\tbikeRental: 'Povol použití sdílených vozidel',\n\trequireBikeTransport: 'Přeprava kola',\n\trequireCarTransport: 'Přeprava auta',\n\tconsiderRentalReturnConstraints: 'Vrať sdílené vozidla během cesty',\n\tdefault: 'default',\n\ttimetableSources: 'Zdroje dát JŘ',\n\ttripCancelled: 'Spoj odřeknut',\n\tstopCancelled: 'Zastávka bez obsluhy',\n\tinOutDisallowed: 'Vstup/výstup není povolen',\n\tinDisallowed: 'Vstup není povolen',\n\toutDisallowed: 'Výstup není povolen',\n\tunscheduledTrip: 'Doplňkový spoj',\n\talertsAvailable: 'Oznámení o provozu',\n\tdataExpiredSince: 'Pozor: Zastaralá data, platná naposledy',\n\tFLEX: 'Poptávková doprava',\n\tWALK: 'Chůze',\n\tBIKE: 'Kolo',\n\tRENTAL: 'Sdílené prostředky',\n\tRIDE_SHARING: 'Spolujízda',\n\tCAR: 'Auto',\n\tCAR_PARKING: 'Auto (využití parkovíšť)',\n\tCAR_DROPOFF: 'Auto (pouze zastavení)',\n\tTRANSIT: 'Hromadná doprava',\n\tTRAM: 'Tramvaj',\n\tSUBWAY: 'Metro',\n\tFERRY: 'Přívoz',\n\tAIRPLANE: 'Letadlo',\n\tSUBURBAN: 'Městská železnice',\n\tBUS: 'Autobus',\n\tCOACH: 'Dálkový autokar',\n\tRAIL: 'Železnice',\n\tHIGHSPEED_RAIL: 'Vysokorychlostní železnice',\n\tLONG_DISTANCE: 'Dálková železnice',\n\tNIGHT_RAIL: 'Noční vlaky',\n\tREGIONAL_FAST_RAIL: 'Zrychlená železnice',\n\tODM: 'Poptávková doprava',\n\tREGIONAL_RAIL: 'Regionální železnice',\n\tOTHER: 'Jiné',\n\troutingSegments: {\n\t\tmaxTransfers: 'Max. počet přestupů',\n\t\tmaxTravelTime: 'Max. čas cesty',\n\t\tfirstMile: 'Přesun k první zastávce',\n\t\tlastMile: 'Přesun od poslední zastávky',\n\t\tdirect: 'Přímé spojení',\n\t\tmaxPreTransitTime: 'Max. čas přesunu',\n\t\tmaxPostTransitTime: 'Max. čas přesunu',\n\t\tmaxDirectTime: 'Max. čas přesunu'\n\t},\n\televationCosts: {\n\t\tNONE: 'Bez odklonů',\n\t\tLOW: 'Malé odklony',\n\t\tHIGH: 'Velké odklony'\n\t},\n\tisochrones: {\n\t\ttitle: 'Izochrony',\n\t\tdisplayLevel: 'Úroveň ukazování',\n\t\tmaxComputeLevel: 'Max. úroveň vypočítání',\n\t\tcanvasRects: 'Čtverce',\n\t\tcanvasCircles: 'Okruhy (zjednodušená projekce)',\n\t\tgeojsonCircles: 'Okruhy (pokročilá projekce)',\n\t\tstyling: 'Styl izochron',\n\t\tnoData: 'Žádné data',\n\t\trequestFailed: 'Chyba žádosti'\n\t},\n\talerts: {\n\t\tvalidFrom: 'Platí od',\n\t\tuntil: 'do',\n\t\tinformation: 'Informace',\n\t\tmore: 'více'\n\t},\n\tRENTAL_BICYCLE: 'Sdílené kolo',\n\tRENTAL_CARGO_BICYCLE: 'Sdílené nákladní kolo',\n\tRENTAL_CAR: 'Sdílené auto',\n\tRENTAL_MOPED: 'Sdílený skútr',\n\tRENTAL_SCOOTER_STANDING: 'Sdílená koloběžka',\n\tRENTAL_SCOOTER_SEATED: 'Sdílená koloběžka se sedačkou',\n\tRENTAL_OTHER: 'Jiné sdílené vozidla',\n\tincline: 'Sklon',\n\tCABLE_CAR: 'Lanová dráha',\n\tFUNICULAR: 'Lanová dráha',\n\tAERIAL_LIFT: 'Lanová dráha',\n\ttoll: 'Pozor! Průjezd tuto trasou je placený.',\n\taccessRestriction: 'Omezený dostup',\n\tcontinuesAs: 'Pokračuje jako',\n\trent: 'Půjčit si',\n\tcopyToClipboard: 'Kopírovat do schránky',\n\trideThroughAllowed: 'Průjezd povolen',\n\trideThroughNotAllowed: 'Průjezd zakázán',\n\trideEndAllowed: 'Parkování povoleno',\n\trideEndNotAllowed: 'Parkování pouze na stanicích',\n\tDEBUG_BUS_ROUTE: 'Trasa autobusu (Debug)',\n\tDEBUG_RAILWAY_ROUTE: 'Trasa vlaku (Debug)',\n\tDEBUG_FERRY_ROUTE: 'Trasa trajektu (Debug)',\n\troutes: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Žádná trasa';\n\t\t\tcase 1:\n\t\t\t\treturn '1 trasa';\n\t\t\tcase 2:\n\t\t\tcase 3:\n\t\t\tcase 4:\n\t\t\t\treturn `${n} trasy`;\n\t\t\tdefault:\n\t\t\t\treturn `${n} tras`;\n\t\t}\n\t}\n};\n\nexport default translations;\n"
  },
  {
    "path": "ui/src/lib/i18n/de.ts",
    "content": "import type { Translations } from './translation';\n\nconst translations: Translations = {\n\tticket: 'Fahrschein',\n\tticketOptions: 'Fahrscheinoptionen',\n\tincludedInTicket: 'Im Fahrschein enthalten',\n\tjourneyDetails: 'Verbindungsdetails',\n\ttransfers: 'Umstiege',\n\twalk: 'Fußweg',\n\tbike: 'Fahrrad',\n\tcargoBike: 'Lastenfahrrad',\n\tscooterStanding: 'Stehroller',\n\tscooterSeated: 'Sitzroller',\n\tcar: 'Auto',\n\ttaxi: 'Taxi',\n\tmoped: 'Moped',\n\tunknownVehicleType: 'Unbekannter Fahrzeugtyp',\n\telectricAssist: 'Elektromotorunterstützung',\n\telectric: 'Elektrisch',\n\tcombustion: 'Benzin',\n\tcombustionDiesel: 'Diesel',\n\thybrid: 'Hybrid',\n\tplugInHybrid: 'Plug-in Hybrid',\n\thydrogenFuelCell: 'Wasserstoff-Brennstoffzelle',\n\tfrom: 'Von',\n\tto: 'Nach',\n\tviaStop: 'Zwischenhalt',\n\tviaStops: 'Zwischenhalte',\n\taddViaStop: 'Zwischenhalt hinzufügen',\n\tremoveViaStop: 'Zwischenhalt entfernen',\n\tviaStayDuration: 'Mindestaufenthalt',\n\tposition: 'Position',\n\tarrival: 'Ankunft',\n\tdeparture: 'Abfahrt',\n\tduration: 'Dauer',\n\tarrivals: 'Ankünfte',\n\tconnections: 'Verbindungen',\n\tdepartures: 'Abfahrten',\n\tlater: 'später',\n\tearlier: 'früher',\n\ttrack: 'Gleis',\n\tplatform: 'Steig',\n\ttrackAbr: 'Gl.',\n\tplatformAbr: 'Stg.',\n\tarrivalOnTrack: 'Ankunft auf Gleis',\n\tswitchToArrivals: 'Wechsel zu Ankünften',\n\tswitchToDepartures: 'Wechsel zu Abfahrten',\n\ttripIntermediateStops: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Fahrt ohne Zwischenhalt';\n\t\t\tcase 1:\n\t\t\t\treturn 'Fahrt eine Station';\n\t\t\tdefault:\n\t\t\t\treturn `Fahrt ${n} Stationen`;\n\t\t}\n\t},\n\tsharingProvider: 'Anbieter',\n\tsharingProviders: 'Anbieter',\n\treturnOnlyAtStations: 'Das Fahrzeug muss an einer Station zurückgegeben werden.',\n\troundtripStationReturnConstraint:\n\t\t'Das Fahrzeug muss wieder an der Abfahrtsstation abgestellt werden.',\n\trentalStation: 'Station',\n\trentalGeofencingZone: 'Zone',\n\tnoItinerariesFound: 'Keine Verbindungen gefunden.',\n\tadvancedSearchOptions: 'Optionen',\n\tselectTransitModes: 'Öffentliche Verkehrsmittel auswählen',\n\tdefaultSelectedModes: 'Alle Verkehrsmittel',\n\tdefaultSelectedProviders: 'Alle Anbieter',\n\tselectElevationCosts: 'Steile Steigungen vermeiden.',\n\tuseRoutedTransfers: 'Geroutete Umstiege verwenden',\n\twheelchair: 'Barrierefreie Umstiege',\n\tbikeRental: 'Sharing-Fahrzeuge berücksichtigen',\n\trequireBikeTransport: 'Fahrradmitnahme',\n\trequireCarTransport: 'Automitnahme',\n\tconsiderRentalReturnConstraints: 'Leihfahrzeuge innerhalb der Reise zurückgeben',\n\tdefault: 'Vorgabe',\n\ttimetableSources: 'Fahrplandatenquellen',\n\ttripCancelled: 'Fahrt entfällt',\n\tstopCancelled: 'Halt entfällt',\n\tinOutDisallowed: 'Ein-/Ausstieg nicht möglich',\n\tinDisallowed: 'Einstieg nicht möglich',\n\toutDisallowed: 'Ausstieg nicht möglich',\n\tunscheduledTrip: 'Zusätzliche Fahrt',\n\talertsAvailable: 'Meldungen liegen vor',\n\tdataExpiredSince: 'Achtung: Veraltete Daten, zuletzt gültig am',\n\tFLEX: 'Bedarfsverkehr',\n\tWALK: 'Zu Fuß',\n\tBIKE: 'Fahrrad',\n\tRENTAL: 'Sharing',\n\tRIDE_SHARING: 'Mitfahrgelegenheit',\n\tCAR: 'Auto',\n\tCAR_DROPOFF: 'Absetzen (Auto)',\n\tCAR_PARKING: 'P+R Park & Ride',\n\tTRANSIT: 'ÖPV',\n\tTRAM: 'Tram',\n\tSUBWAY: 'U-Bahn',\n\tFERRY: 'Fähre',\n\tAIRPLANE: 'Flugzeug',\n\tSUBURBAN: 'S-Bahn',\n\tBUS: 'Bus',\n\tCOACH: 'Reisebus',\n\tRAIL: 'Zug',\n\tHIGHSPEED_RAIL: 'Hochgeschwindigkeitszug',\n\tLONG_DISTANCE: 'Intercityzug',\n\tNIGHT_RAIL: 'Nachtzug',\n\tREGIONAL_FAST_RAIL: 'Schnellzug',\n\tODM: 'Bedarfsverkehr',\n\tREGIONAL_RAIL: 'Regionalzug',\n\tOTHER: 'Andere',\n\troutingSegments: {\n\t\tmaxTransfers: 'Max. Umstiege',\n\t\tmaxTravelTime: 'Max. Reisezeit',\n\t\tfirstMile: 'Erste Meile',\n\t\tlastMile: 'Letzte Meile',\n\t\tdirect: 'Direktverbindung',\n\t\tmaxPreTransitTime: 'Max. Vorlaufzeit',\n\t\tmaxPostTransitTime: 'Max. Nachlaufzeit',\n\t\tmaxDirectTime: 'Max. Direktzeit'\n\t},\n\televationCosts: { NONE: 'Keine Umwege', LOW: 'Kleine Umwege', HIGH: 'Große Umwege' },\n\tisochrones: {\n\t\ttitle: 'Isochronen',\n\t\tdisplayLevel: 'Darstellungsebene',\n\t\tmaxComputeLevel: 'Max. Berechnungsebene',\n\t\tcanvasRects: 'Rechtecke (Overlay)',\n\t\tcanvasCircles: 'Kreise (Overlay)',\n\t\tgeojsonCircles: 'Kreise (Geometrie)',\n\t\tstyling: 'Darstellung der Isochronen',\n\t\tnoData: 'Keine Daten',\n\t\trequestFailed: 'Anfrage fehlgeschlagen'\n\t},\n\talerts: {\n\t\tvalidFrom: 'Gültig von',\n\t\tuntil: 'bis',\n\t\tinformation: 'Informationen',\n\t\tmore: 'mehr'\n\t},\n\tRENTAL_BICYCLE: 'Bikesharing',\n\tRENTAL_CARGO_BICYCLE: 'Lastenrad Sharing',\n\tRENTAL_CAR: 'Car Sharing',\n\tRENTAL_MOPED: 'Moped Sharing',\n\tRENTAL_SCOOTER_STANDING: 'Scooter Sharing',\n\tRENTAL_SCOOTER_SEATED: 'Sitzroller Sharing',\n\tRENTAL_OTHER: 'Anderes sharing Fahrzeug',\n\tincline: 'Steigung',\n\tCABLE_CAR: 'Seilbahn',\n\tFUNICULAR: 'Standseilbahn',\n\tAERIAL_LIFT: 'Luftseilbahn',\n\ttoll: 'Achtung! Mautpflichtige Straße.',\n\taccessRestriction: 'Kein Zugang',\n\tcontinuesAs: 'Weiter als',\n\trent: 'Ausleihen',\n\tcopyToClipboard: 'In die Zwischenablage kopieren',\n\trideThroughAllowed: 'Durchfahrt erlaubt',\n\trideThroughNotAllowed: 'Durchfahrt verboten',\n\trideEndAllowed: 'Parken erlaubt',\n\trideEndNotAllowed: 'Parken nur an Stationen',\n\tDEBUG_BUS_ROUTE: 'Busroute (Debug)',\n\tDEBUG_RAILWAY_ROUTE: 'Bahnroute (Debug)',\n\tDEBUG_FERRY_ROUTE: 'Fährenroute (Debug)',\n\troutes: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Keine Route';\n\t\t\tcase 1:\n\t\t\t\treturn '1 Route';\n\t\t\tdefault:\n\t\t\t\treturn `${n} Routen`;\n\t\t}\n\t}\n};\n\nexport default translations;\n"
  },
  {
    "path": "ui/src/lib/i18n/en.ts",
    "content": "import type { Translations } from './translation';\n\nconst translations: Translations = {\n\tticket: 'Ticket',\n\tticketOptions: 'Ticket Options',\n\tincludedInTicket: 'Included in ticket',\n\tjourneyDetails: 'Journey Details',\n\ttransfers: 'transfers',\n\twalk: 'Walk',\n\tbike: 'Bike',\n\tcargoBike: 'Cargo bike',\n\tscooterStanding: 'Standing kick scooter',\n\tscooterSeated: 'Seated kick scooter',\n\tcar: 'Car',\n\ttaxi: 'Taxi',\n\tmoped: 'Moped',\n\tunknownVehicleType: 'Unknown vehicle type',\n\telectricAssist: 'Electric motor assist',\n\telectric: 'Electric',\n\tcombustion: 'Combustion',\n\tcombustionDiesel: 'Diesel',\n\thybrid: 'Hybrid',\n\tplugInHybrid: 'Plug-in hybrid',\n\thydrogenFuelCell: 'Hydrogen fuel cell',\n\tfrom: 'From',\n\tto: 'To',\n\tviaStop: 'Via stop',\n\tviaStops: 'Via stops',\n\taddViaStop: 'Add via stop',\n\tremoveViaStop: 'Remove via stop',\n\tviaStayDuration: 'Minimum stay',\n\tposition: 'Position',\n\tarrival: 'Arrival',\n\tdeparture: 'Departure',\n\tconnections: 'Connections',\n\tduration: 'Duration',\n\tarrivals: 'Arrivals',\n\tlater: 'later',\n\tearlier: 'earlier',\n\tdepartures: 'Departures',\n\tswitchToArrivals: 'Switch to arrivals',\n\tswitchToDepartures: 'Switch to departures',\n\ttrack: 'Platform',\n\tplatform: 'Platform',\n\ttrackAbr: 'Pl.',\n\tplatformAbr: 'Pl.',\n\tarrivalOnTrack: 'Arrival on track',\n\ttripIntermediateStops: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'No intermediate stops';\n\t\t\tcase 1:\n\t\t\t\treturn '1 intermediate stop';\n\t\t\tdefault:\n\t\t\t\treturn `${n} intermediate stops`;\n\t\t}\n\t},\n\tsharingProvider: 'Provider',\n\tsharingProviders: 'Providers',\n\treturnOnlyAtStations: 'The vehicle must be returned at a station.',\n\troundtripStationReturnConstraint: 'The vehicle must be returned to the departure station.',\n\trentalStation: 'Station',\n\trentalGeofencingZone: 'Zone',\n\tnoItinerariesFound: 'No itineraries found.',\n\tadvancedSearchOptions: 'Options',\n\tselectTransitModes: 'Select transit modes',\n\tdefaultSelectedModes: 'All transit modes',\n\tdefaultSelectedProviders: 'All providers',\n\tselectElevationCosts: 'Avoid steep incline.',\n\tuseRoutedTransfers: 'Use routed transfers',\n\twheelchair: 'Accessible transfers',\n\tbikeRental: 'Allow usage of sharing vehicles',\n\trequireBikeTransport: 'Bike carriage',\n\trequireCarTransport: 'Car carriage',\n\tconsiderRentalReturnConstraints: 'Return rental vehicles within journey',\n\tdefault: 'Default',\n\ttimetableSources: 'Timetable sources',\n\ttripCancelled: 'Trip cancelled',\n\tstopCancelled: 'Stop cancelled',\n\tinOutDisallowed: 'Entry/exit not possible',\n\tinDisallowed: 'Entry not possible',\n\toutDisallowed: 'Exit not possible',\n\tunscheduledTrip: 'Additional service',\n\talertsAvailable: 'Service alerts present',\n\tdataExpiredSince: 'Warning: Expired data, last valid',\n\tFLEX: 'On-Demand',\n\tWALK: 'Walking',\n\tBIKE: 'Bike',\n\tRENTAL: 'Sharing',\n\tRIDE_SHARING: 'Ride sharing',\n\tCAR: 'Car',\n\tCAR_PARKING: 'Car Parking',\n\tCAR_DROPOFF: 'Drop-off (car)',\n\tTRANSIT: 'Transit',\n\tTRAM: 'Tram',\n\tSUBWAY: 'Subway',\n\tFERRY: 'Ferry',\n\tAIRPLANE: 'Airplane',\n\tSUBURBAN: 'Suburban Rail',\n\tBUS: 'Bus',\n\tCOACH: 'Long Distance Bus / Coach',\n\tRAIL: 'Train',\n\tHIGHSPEED_RAIL: 'High Speed Rail',\n\tLONG_DISTANCE: 'Intercity Rail',\n\tNIGHT_RAIL: 'Night Rail',\n\tREGIONAL_FAST_RAIL: 'Fast Rail',\n\tODM: 'On-Demand Mobility',\n\tREGIONAL_RAIL: 'Regional Rail',\n\tOTHER: 'Other',\n\tRENTAL_BICYCLE: 'Shared bike',\n\tRENTAL_CARGO_BICYCLE: 'Shared cargo bike',\n\tRENTAL_CAR: 'Shared car',\n\tRENTAL_MOPED: 'Shared moped',\n\tRENTAL_SCOOTER_STANDING: 'Shared standing scooter',\n\tRENTAL_SCOOTER_SEATED: 'Shared seated scooter',\n\tRENTAL_OTHER: 'Other shared vehicle',\n\tCABLE_CAR: 'Cable car',\n\tFUNICULAR: 'Funicular',\n\tAERIAL_LIFT: 'Aerial lift',\n\troutingSegments: {\n\t\tmaxTransfers: 'Max. transfers',\n\t\tmaxTravelTime: 'Max. travel time',\n\t\tfirstMile: 'First mile',\n\t\tlastMile: 'Last mile',\n\t\tdirect: 'Direct connection',\n\t\tmaxPreTransitTime: 'Max. pre-transit time',\n\t\tmaxPostTransitTime: 'Max. post-transit time',\n\t\tmaxDirectTime: 'Max. direct time'\n\t},\n\televationCosts: {\n\t\tNONE: 'No detours',\n\t\tLOW: 'Small detours',\n\t\tHIGH: 'Large detours'\n\t},\n\tisochrones: {\n\t\ttitle: 'Isochrones',\n\t\tdisplayLevel: 'Display level',\n\t\tmaxComputeLevel: 'Max. computation level',\n\t\tcanvasRects: 'Rects (Overlay)',\n\t\tcanvasCircles: 'Circles (Overlay)',\n\t\tgeojsonCircles: 'Circles (Geometry)',\n\t\tstyling: 'Isochrones styling',\n\t\tnoData: 'No data',\n\t\trequestFailed: 'Request failed'\n\t},\n\talerts: {\n\t\tvalidFrom: 'Valid from',\n\t\tuntil: 'until',\n\t\tinformation: 'Information',\n\t\tmore: 'more'\n\t},\n\tincline: 'Incline',\n\ttoll: 'Warning! A fee must be paid to use this route.',\n\taccessRestriction: 'No access',\n\tcontinuesAs: 'Continues as',\n\trent: 'Rent',\n\tcopyToClipboard: 'Copy to clipboard',\n\trideThroughAllowed: 'Riding through allowed',\n\trideThroughNotAllowed: 'Riding through not allowed',\n\trideEndAllowed: 'Parking allowed',\n\trideEndNotAllowed: 'Parking only at stations',\n\tDEBUG_BUS_ROUTE: 'Bus Route (Debug)',\n\tDEBUG_RAILWAY_ROUTE: 'Railway Route (Debug)',\n\tDEBUG_FERRY_ROUTE: 'Ferry Route (Debug)',\n\troutes: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'No routes';\n\t\t\tcase 1:\n\t\t\t\treturn '1 route';\n\t\t\tdefault:\n\t\t\t\treturn `${n} routes`;\n\t\t}\n\t}\n};\n\nexport default translations;\n"
  },
  {
    "path": "ui/src/lib/i18n/fr.ts",
    "content": "import type { Translations } from './translation';\n\nconst translations: Translations = {\n\tticket: 'Billet',\n\tticketOptions: 'Options de billet',\n\tincludedInTicket: 'Inclus dans le billet',\n\tjourneyDetails: 'Détails du voyage',\n\twalk: 'à pied',\n\tbike: 'Vélo',\n\tcargoBike: 'Vélo Cargo',\n\tscooterStanding: 'Trottinette',\n\tscooterSeated: 'Trottinette avec siège',\n\tcar: 'Voiture',\n\ttaxi: 'Taxi',\n\tmoped: 'Mobylette',\n\tunknownVehicleType: 'Type de véhicule inconnu',\n\telectricAssist: 'Assistance moteur électrique',\n\telectric: 'Électrique',\n\tcombustion: 'Combustion',\n\tcombustionDiesel: 'Diesel',\n\thybrid: 'Hybride',\n\tplugInHybrid: 'Hybride rechargeable',\n\thydrogenFuelCell: 'Pile à combustible hydrogène',\n\tfrom: 'De',\n\tto: 'À',\n\tviaStop: 'Arrêt intermédiaire',\n\tviaStops: 'Arrêts intermédiaires',\n\taddViaStop: 'Ajouter un arrêt intermédiaire',\n\tremoveViaStop: \"Supprimer l'arrêt intermédiaire\",\n\tviaStayDuration: \"Durée minimale d'arrêt\",\n\tposition: 'Position',\n\tarrival: 'Arrivée',\n\tdeparture: 'Départ',\n\tduration: 'Durée',\n\tarrivals: 'Arrivées',\n\tlater: 'plus tard',\n\tearlier: 'plus tôt',\n\tdepartures: 'Départs',\n\tconnections: 'Itinéraires',\n\tswitchToArrivals: 'Afficher les arrivées',\n\tswitchToDepartures: 'Afficher les départs',\n\ttrack: 'Voie',\n\tplatform: 'Quai',\n\ttrackAbr: 'V.',\n\tplatformAbr: 'Q.',\n\tarrivalOnTrack: 'Arrivée sur la voie',\n\ttripIntermediateStops: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Aucun arrêt intermédiaire';\n\t\t\tcase 1:\n\t\t\t\treturn '1 arrêt intermédiaire';\n\t\t\tdefault:\n\t\t\t\treturn `${n} arrêts intermédiaires`;\n\t\t}\n\t},\n\tsharingProvider: 'Fournisseur',\n\tsharingProviders: 'Fournisseurs',\n\ttransfers: 'correspondances',\n\treturnOnlyAtStations: 'Le véhicule doit être retourné à une station.',\n\troundtripStationReturnConstraint: 'Le véhicule doit être retourné à la station de départ.',\n\trentalStation: 'Station',\n\trentalGeofencingZone: 'Zone',\n\tnoItinerariesFound: 'Aucun itinéraire trouvé.',\n\tadvancedSearchOptions: 'Options',\n\tselectTransitModes: 'Sélectionner les modes de transport en commun',\n\tdefaultSelectedModes: 'Tous les transports en commun',\n\tdefaultSelectedProviders: 'Tous les fournisseurs',\n\tselectElevationCosts: 'Évitez les pentes abruptes.', // TODO Online translated\n\twheelchair: 'Correspondances accessibles',\n\tuseRoutedTransfers: 'Utiliser les correspondances routées',\n\tbikeRental: 'Utiliser véhicules partagés',\n\trequireBikeTransport: 'Transport vélo',\n\trequireCarTransport: 'Transport voiture',\n\tconsiderRentalReturnConstraints: 'Retourner les véhicules partagés pendant le voyage',\n\tdefault: 'Faire défaut',\n\ttimetableSources: 'Sources des horaires',\n\ttripCancelled: 'Voyage annulé',\n\tstopCancelled: 'Arrêt supprimé',\n\tinOutDisallowed: 'Impossible de monter/descendre',\n\tinDisallowed: 'Impossible de monter',\n\toutDisallowed: 'Impossible de descendre',\n\tunscheduledTrip: 'Voyage supplémentaire',\n\talertsAvailable: 'Annonces disponibles',\n\tdataExpiredSince: 'Attention : Données expirées, valides jusqu’au',\n\tFLEX: 'Transport à la demande',\n\tWALK: 'À pied',\n\tBIKE: 'Vélo',\n\tRENTAL: 'Loué',\n\tRIDE_SHARING: 'Covoiturage',\n\tCAR: 'Voiture',\n\tCAR_PARKING: 'Garage voiture',\n\tCAR_DROPOFF: 'Dépose (voiture)',\n\tTRANSIT: 'Transports en commun',\n\tTRAM: 'Tram',\n\tSUBWAY: 'Métro',\n\tFERRY: 'Ferry',\n\tAIRPLANE: 'Avion',\n\tSUBURBAN: 'RER',\n\tBUS: 'Bus',\n\tCOACH: 'Autocar',\n\tRAIL: 'Train',\n\tHIGHSPEED_RAIL: 'Train à grande vitesse',\n\tLONG_DISTANCE: 'Train intercité',\n\tNIGHT_RAIL: 'Train de nuit',\n\tREGIONAL_FAST_RAIL: 'Train express',\n\tODM: 'Transport à la demande',\n\tREGIONAL_RAIL: 'Train régional',\n\tOTHER: 'Autres',\n\troutingSegments: {\n\t\tmaxTransfers: 'Max. de correspondances',\n\t\tmaxTravelTime: 'Temps de trajet max.',\n\t\tfirstMile: 'Premier kilomètre',\n\t\tlastMile: 'Dernier kilomètre',\n\t\tdirect: 'Connexion directe',\n\t\tmaxPreTransitTime: 'Durée max. avant transit',\n\t\tmaxPostTransitTime: 'Durée max. après transit',\n\t\tmaxDirectTime: 'Durée max. directe'\n\t},\n\televationCosts: {\n\t\tNONE: 'Pas de détours',\n\t\tLOW: 'Petits détours',\n\t\tHIGH: 'Grands détours'\n\t},\n\tisochrones: {\n\t\ttitle: 'Isochrones',\n\t\tdisplayLevel: 'Couche de présentation',\n\t\tmaxComputeLevel: 'Niveau de calcul max.',\n\t\tcanvasRects: 'Rectes (Superposer)',\n\t\tcanvasCircles: 'Cercles (Superposer)',\n\t\tgeojsonCircles: 'Circles (Géométrie)',\n\t\tstyling: 'Style pour Isochrone',\n\t\tnoData: 'Pas de données',\n\t\trequestFailed: 'Échec de la demande'\n\t},\n\talerts: {\n\t\tvalidFrom: 'Valable du',\n\t\tuntil: 'au',\n\t\tinformation: 'Informations',\n\t\tmore: 'de plus'\n\t},\n\tRENTAL_BICYCLE: 'Vélo partagé',\n\tRENTAL_CARGO_BICYCLE: 'Vélo cargo partagé',\n\tRENTAL_CAR: 'Voiture partagée',\n\tRENTAL_MOPED: 'Mobylette partagée',\n\tRENTAL_SCOOTER_STANDING: 'Trottinette debout partagée',\n\tRENTAL_SCOOTER_SEATED: 'Trottinette assise partagée',\n\tRENTAL_OTHER: 'Autre véhicule partagé',\n\tincline: 'Pente',\n\tCABLE_CAR: 'Téléphérique',\n\tFUNICULAR: 'Funiculaire',\n\tAERIAL_LIFT: 'Remontée mécanique',\n\ttoll: 'Attention ! Route à péage.',\n\taccessRestriction: 'Accès restreint',\n\tcontinuesAs: 'Continue comme',\n\trent: 'Louer',\n\tcopyToClipboard: 'Copier dans le presse-papiers',\n\trideThroughAllowed: 'Passage autorisé',\n\trideThroughNotAllowed: 'Passage non autorisé',\n\trideEndAllowed: 'Stationnement autorisé',\n\trideEndNotAllowed: 'Stationnement uniquement aux stations',\n\tDEBUG_BUS_ROUTE: 'Itinéraire de bus (Debug)',\n\tDEBUG_RAILWAY_ROUTE: 'Itinéraire ferroviaire (Debug)',\n\tDEBUG_FERRY_ROUTE: 'Itinéraire de ferry (Debug)',\n\troutes: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Aucun itinéraire';\n\t\t\tcase 1:\n\t\t\t\treturn '1 itinéraire';\n\t\t\tdefault:\n\t\t\t\treturn `${n} itinéraires`;\n\t\t}\n\t}\n};\n\nexport default translations;\n"
  },
  {
    "path": "ui/src/lib/i18n/pl.ts",
    "content": "import type { Translations } from './translation';\n\nconst translations: Translations = {\n\tticket: 'Bilet',\n\tticketOptions: 'Opcje biletu',\n\tincludedInTicket: 'Zawarte w ramach biletu',\n\tjourneyDetails: 'Szczegóły podróży',\n\ttransfers: 'przesiadki',\n\twalk: 'Pieszo',\n\tbike: 'Rower',\n\tcargoBike: 'Rower cargo',\n\tscooterStanding: 'Hulajnoga stojąca',\n\tscooterSeated: 'Hulajnoga z siedziskiem',\n\tcar: 'Samochód',\n\ttaxi: 'Taksówka',\n\tmoped: 'Skuter',\n\tunknownVehicleType: 'Nieznany typ pojazdu',\n\telectricAssist: 'Wspomaganie elektryczne',\n\telectric: 'Elektryczny',\n\tcombustion: 'Spalinowy',\n\tcombustionDiesel: 'Diesel',\n\thybrid: 'Hybrydowy',\n\tplugInHybrid: 'Hybryda plug-in',\n\thydrogenFuelCell: 'Ogniwo paliwowe na wodór',\n\tfrom: 'Z',\n\tto: 'Do',\n\tviaStop: 'Przystanek pośredni',\n\tviaStops: 'Przystanki pośrednie',\n\taddViaStop: 'Dodaj przystanek pośredni',\n\tremoveViaStop: 'Usuń przystanek pośredni',\n\tviaStayDuration: 'Minimalny postój',\n\tposition: 'Pozycja',\n\tarrival: 'Przyjazd',\n\tdeparture: 'Odjazd',\n\tduration: 'Czas trwania',\n\tarrivals: 'Przyjazdy',\n\tlater: 'później',\n\tearlier: 'wcześniej',\n\tdepartures: 'Odjazdy',\n\tconnections: 'Połączenia',\n\tswitchToArrivals: 'Przełącz na przyjazdy',\n\tswitchToDepartures: 'Przełącz na odjazdy',\n\ttrack: 'Tor',\n\tplatform: 'Peron',\n\ttrackAbr: 'T.',\n\tplatformAbr: 'Pr.',\n\tarrivalOnTrack: 'Przyjazd na tor',\n\ttripIntermediateStops: (n: number) => {\n\t\tif (n == 0) {\n\t\t\treturn 'Brak przystanków pośrednich';\n\t\t}\n\t\tif (n == 1) {\n\t\t\treturn '1 przystanek pośredni';\n\t\t}\n\t\tif (n % 10 > 1 && n % 10 < 5 && n != 12 && n != 13 && n != 14) {\n\t\t\treturn `${n} przystanki pośrednie`;\n\t\t}\n\t\treturn `${n} przystanków pośrednich`;\n\t},\n\tsharingProvider: 'Dostawca danych',\n\tsharingProviders: 'Dostawcy danych',\n\treturnOnlyAtStations: 'Pojazd musi zostać zwrócony na stacji.',\n\troundtripStationReturnConstraint: 'Pojazd musi zostać zwrócony do stacji początkowej.',\n\trentalStation: 'Stacja',\n\trentalGeofencingZone: 'Strefa',\n\tnoItinerariesFound: 'Nie znaleziono połączeń.',\n\tadvancedSearchOptions: 'Opcje',\n\tselectTransitModes: 'Wybierz środki transportu',\n\tdefaultSelectedModes: 'Wszystkie środki transportu',\n\tdefaultSelectedProviders: 'Wszyscy dostawcy',\n\tselectElevationCosts: 'Unikaj stromych nachyleń.',\n\twheelchair: 'Bezbarierowe przesiadki',\n\tuseRoutedTransfers: 'Wyznacz trasy dla przesiadek',\n\tbikeRental: 'Użyj pojazdów współdzielonych',\n\trequireBikeTransport: 'Przewóz roweru',\n\trequireCarTransport: 'Przewóz samochodu',\n\tconsiderRentalReturnConstraints: 'Zwróć pojazd współdzielony podczas podróży',\n\tdefault: 'Domyślne',\n\ttimetableSources: 'Źródła danych rozkładowych',\n\ttripCancelled: 'Kurs odwołany',\n\tstopCancelled: 'Przystanek nieobsługiwany',\n\tinOutDisallowed: 'Zabronione wejście i wyjście',\n\tinDisallowed: 'Zabronione wejście',\n\toutDisallowed: 'Zabronione wyjście',\n\tunscheduledTrip: 'Kurs dodatkowy',\n\talertsAvailable: 'Istnieją ogłoszenia',\n\tdataExpiredSince: 'Uwaga: Dane nieaktualne, ostatnio ważne',\n\tFLEX: 'Transport na żądanie',\n\tWALK: 'Pieszo',\n\tBIKE: 'Rower',\n\tRENTAL: 'Współdzielenie pojazdów',\n\tRIDE_SHARING: 'Wspólne przejazdy',\n\tCAR: 'Samochód',\n\tCAR_PARKING: 'Samochód (użyj parkingów)',\n\tCAR_DROPOFF: 'Samochód (tylko zatrzymanie)',\n\tTRANSIT: 'Transport publiczny',\n\tTRAM: 'Tramwaj',\n\tSUBWAY: 'Metro',\n\tFERRY: 'Prom',\n\tAIRPLANE: 'Samolot',\n\tSUBURBAN: 'Kolej miejska',\n\tBUS: 'Autobus',\n\tCOACH: 'Autokar dalekobieżny',\n\tRAIL: 'Kolej',\n\tHIGHSPEED_RAIL: 'Kolej dużych prędkości',\n\tLONG_DISTANCE: 'Kolej dalekobieżna',\n\tNIGHT_RAIL: 'Nocne pociągi',\n\tREGIONAL_FAST_RAIL: 'Pociąg pospieszny',\n\tODM: 'Transport na żądanie',\n\tREGIONAL_RAIL: 'Kolej regionalna',\n\tOTHER: 'Inne',\n\troutingSegments: {\n\t\tmaxTransfers: 'Maks. ilość przesiadek',\n\t\tmaxTravelTime: 'Maks. czas podróży',\n\t\tfirstMile: 'Początek podróży',\n\t\tlastMile: 'Osiągnięcie celu',\n\t\tdirect: 'Połączenie bezpośrednie',\n\t\tmaxPreTransitTime: 'Maks. czas dotarcia',\n\t\tmaxPostTransitTime: 'Maks. czas dotarcia',\n\t\tmaxDirectTime: 'Maks. czas dotarcia'\n\t},\n\televationCosts: {\n\t\tNONE: 'Bez odchyleń od trasy',\n\t\tLOW: 'Małe odchylenia od trasy',\n\t\tHIGH: 'Duże odchylenia od trasy'\n\t},\n\tisochrones: {\n\t\ttitle: 'Izochrony',\n\t\tdisplayLevel: 'Poziom wyświetlania',\n\t\tmaxComputeLevel: 'Maks. poziom wyliczenia',\n\t\tcanvasRects: 'Kwadraty (warstwa)',\n\t\tcanvasCircles: 'Okręgi (warstwa)',\n\t\tgeojsonCircles: 'Okręgi (geometria)',\n\t\tstyling: 'Styl izochron',\n\t\tnoData: 'Brak danych',\n\t\trequestFailed: 'Błąd zapytania'\n\t},\n\talerts: {\n\t\tvalidFrom: 'Ważne od',\n\t\tuntil: 'do',\n\t\tinformation: 'Informacje',\n\t\tmore: 'więcej'\n\t},\n\tRENTAL_BICYCLE: 'Rower współdzielony',\n\tRENTAL_CARGO_BICYCLE: 'Rower cargo współdzielony',\n\tRENTAL_CAR: 'Samochód współdzielony',\n\tRENTAL_MOPED: 'Skuter współdzielony',\n\tRENTAL_SCOOTER_STANDING: 'Hulajnoga stojąca współdzielona',\n\tRENTAL_SCOOTER_SEATED: 'Hulajnoga z siedziskiem współdzielona',\n\tRENTAL_OTHER: 'Inny pojazd współdzielony',\n\tincline: 'Nachylenie',\n\tCABLE_CAR: 'Kolej linowa',\n\tFUNICULAR: 'Kolej linowo-terenowa',\n\tAERIAL_LIFT: 'Wyciąg krzesełkowy',\n\ttoll: 'Uwaga! Za przejazd tą trasą pobierana jest opłata.',\n\taccessRestriction: 'Ograniczony dostęp',\n\tcontinuesAs: 'Kontynuuje jako',\n\trent: 'Wypożycz',\n\tcopyToClipboard: 'Kopiuj do schowka',\n\trideThroughAllowed: 'Przejazd dozwolony',\n\trideThroughNotAllowed: 'Przejazd niedozwolony',\n\trideEndAllowed: 'Parkowanie dozwolone',\n\trideEndNotAllowed: 'Parkowanie tylko na stacjach',\n\tDEBUG_BUS_ROUTE: 'Trasa autobusu (Debug)',\n\tDEBUG_RAILWAY_ROUTE: 'Trasa kolejowa (Debug)',\n\tDEBUG_FERRY_ROUTE: 'Trasa promu (Debug)',\n\troutes: (n: number) => {\n\t\tswitch (n) {\n\t\t\tcase 0:\n\t\t\t\treturn 'Brak trasy';\n\t\t\tcase 1:\n\t\t\t\treturn '1 trasa';\n\t\t\tdefault:\n\t\t\t\treturn `${n} trasy`;\n\t\t}\n\t}\n};\n\nexport default translations;\n"
  },
  {
    "path": "ui/src/lib/i18n/translation.ts",
    "content": "import { browser } from '$app/environment';\nimport bg from './bg';\nimport en from './en';\nimport de from './de';\nimport fr from './fr';\nimport pl from './pl';\nimport cs from './cs';\n\nexport type Translations = {\n\tticket: string;\n\tticketOptions: string;\n\tincludedInTicket: string;\n\tjourneyDetails: string;\n\ttransfers: string;\n\twalk: string;\n\tbike: string;\n\tcargoBike: string;\n\tscooterStanding: string;\n\tscooterSeated: string;\n\tcar: string;\n\ttaxi: string;\n\tmoped: string;\n\tunknownVehicleType: string;\n\telectricAssist: string;\n\telectric: string;\n\tcombustion: string;\n\tcombustionDiesel: string;\n\thybrid: string;\n\tplugInHybrid: string;\n\thydrogenFuelCell: string;\n\tfrom: string;\n\tto: string;\n\tviaStop: string;\n\tviaStops: string;\n\taddViaStop: string;\n\tremoveViaStop: string;\n\tviaStayDuration: string;\n\tposition: string;\n\tarrival: string;\n\tdeparture: string;\n\tduration: string;\n\tlater: string;\n\tearlier: string;\n\tarrivals: string;\n\tdepartures: string;\n\tconnections: string;\n\tswitchToArrivals: string;\n\tswitchToDepartures: string;\n\tarrivalOnTrack: string;\n\ttrack: string;\n\tplatform: string;\n\ttrackAbr: string;\n\tplatformAbr: string;\n\ttripIntermediateStops: (n: number) => string;\n\tsharingProvider: string;\n\tsharingProviders: string;\n\treturnOnlyAtStations: string;\n\troundtripStationReturnConstraint: string;\n\trentalStation: string;\n\trentalGeofencingZone: string;\n\tnoItinerariesFound: string;\n\tadvancedSearchOptions: string;\n\tselectTransitModes: string;\n\tdefaultSelectedModes: string;\n\tdefaultSelectedProviders: string;\n\tselectElevationCosts: string;\n\twheelchair: string;\n\tuseRoutedTransfers: string;\n\tbikeRental: string;\n\trequireBikeTransport: string;\n\trequireCarTransport: string;\n\tconsiderRentalReturnConstraints: string;\n\tdefault: string;\n\ttimetableSources: string;\n\ttripCancelled: string;\n\tstopCancelled: string;\n\tinOutDisallowed: string;\n\tinDisallowed: string;\n\toutDisallowed: string;\n\tunscheduledTrip: string;\n\talertsAvailable: string;\n\tdataExpiredSince: string;\n\tFLEX: string;\n\tWALK: string;\n\tBIKE: string;\n\tRENTAL: string;\n\tCAR: string;\n\tCAR_DROPOFF: string;\n\tCAR_PARKING: string;\n\tTRANSIT: string;\n\tTRAM: string;\n\tSUBWAY: string;\n\tFERRY: string;\n\tAIRPLANE: string;\n\tSUBURBAN: string;\n\tBUS: string;\n\tCOACH: string;\n\tRAIL: string;\n\tHIGHSPEED_RAIL: string;\n\tLONG_DISTANCE: string;\n\tNIGHT_RAIL: string;\n\tREGIONAL_FAST_RAIL: string;\n\tODM: string;\n\tRIDE_SHARING: string;\n\tREGIONAL_RAIL: string;\n\tOTHER: string;\n\troutingSegments: {\n\t\tmaxTransfers: string;\n\t\tmaxTravelTime: string;\n\t\tfirstMile: string;\n\t\tlastMile: string;\n\t\tdirect: string;\n\t\tmaxPreTransitTime: string;\n\t\tmaxPostTransitTime: string;\n\t\tmaxDirectTime: string;\n\t};\n\televationCosts: {\n\t\tNONE: string;\n\t\tLOW: string;\n\t\tHIGH: string;\n\t};\n\tisochrones: {\n\t\ttitle: string;\n\t\tdisplayLevel: string;\n\t\tmaxComputeLevel: string;\n\t\tcanvasRects: string;\n\t\tcanvasCircles: string;\n\t\tgeojsonCircles: string;\n\t\tstyling: string;\n\t\tnoData: string;\n\t\trequestFailed: string;\n\t};\n\talerts: {\n\t\tvalidFrom: string;\n\t\tuntil: string;\n\t\tinformation: string;\n\t\tmore: string;\n\t};\n\tRENTAL_BICYCLE: string;\n\tRENTAL_CARGO_BICYCLE: string;\n\tRENTAL_CAR: string;\n\tRENTAL_MOPED: string;\n\tRENTAL_SCOOTER_STANDING: string;\n\tRENTAL_SCOOTER_SEATED: string;\n\tRENTAL_OTHER: string;\n\tincline: string;\n\tCABLE_CAR: string;\n\tFUNICULAR: string;\n\tAERIAL_LIFT: string;\n\ttoll: string;\n\taccessRestriction: string;\n\tcontinuesAs: string;\n\tDEBUG_BUS_ROUTE: string;\n\tDEBUG_RAILWAY_ROUTE: string;\n\tDEBUG_FERRY_ROUTE: string;\n\trent: string;\n\tcopyToClipboard: string;\n\trideThroughAllowed: string;\n\trideThroughNotAllowed: string;\n\trideEndAllowed: string;\n\trideEndNotAllowed: string;\n\troutes: (n: number) => string;\n};\n\nconst translations: Map<string, Translations> = new Map(\n\tObject.entries({\n\t\tbg,\n\t\tpl,\n\t\ten,\n\t\tde,\n\t\tfr,\n\t\tcs\n\t})\n);\n\nconst urlLanguage = browser\n\t? new URLSearchParams(window.location.search).get('language')\n\t: undefined;\n\nconst translationsKey = (\n\turlLanguage && translations.get(urlLanguage ?? '')\n\t\t? urlLanguage\n\t\t: browser\n\t\t\t? (navigator.languages.find((l) => translations.has(l.slice(0, 2))) ?? 'en')\n\t\t\t: 'en'\n)?.slice(0, 2);\n\nexport const language = urlLanguage ?? translationsKey;\nexport const t = translationsKey ? translations.get(translationsKey)! : en;\n"
  },
  {
    "path": "ui/src/lib/lngLatToStr.ts",
    "content": "import maplibregl from 'maplibre-gl';\n\nexport function lngLatToStr(pos: maplibregl.LngLatLike) {\n\tconst p = maplibregl.LngLat.convert(pos);\n\treturn `${p.lat},${p.lng}`;\n}\n"
  },
  {
    "path": "ui/src/lib/map/Control.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn } from '$lib/utils';\n\timport type { Map, ControlPosition, IControl } from 'maplibre-gl';\n\timport { getContext, onDestroy, type Snippet } from 'svelte';\n\n\tlet {\n\t\tchildren,\n\t\tposition,\n\t\tclass: className\n\t}: {\n\t\tchildren?: Snippet;\n\t\tposition?: ControlPosition;\n\t\tclass?: string;\n\t} = $props();\n\tlet el: HTMLElement | null = null;\n\tlet initialized = $state(false);\n\n\tclass Control implements IControl {\n\t\t/* eslint-disable-next-line */\n\t\tonAdd(map: Map): HTMLElement {\n\t\t\treturn el!;\n\t\t}\n\t\t/* eslint-disable-next-line */\n\t\tonRemove(map: Map): void {\n\t\t\tel?.parentNode?.removeChild(el);\n\t\t}\n\t\tgetDefaultPosition?: (() => ControlPosition) | undefined;\n\t}\n\n\tlet ctrl = new Control();\n\tlet ctx: { map: Map | null } = getContext('map');\n\n\t$effect(() => {\n\t\tif (ctx.map && el && position != undefined) {\n\t\t\tctx.map.addControl(ctrl, position);\n\t\t\tinitialized = true;\n\t\t}\n\t});\n\n\tonDestroy(() => ctx.map?.removeControl(ctrl));\n</script>\n\n<div\n\tclass:hidden={!initialized && position != undefined}\n\tclass={cn('clear-both pointer-events-auto pt-2 md:pt-4 px-2 md:px-4 max-w-full', className)}\n\tbind:this={el}\n>\n\t{#if children}\n\t\t{@render children()}\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/map/Drawer.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn } from '$lib/utils.js';\n\timport { ChevronDown } from '@lucide/svelte';\n\timport { onMount, type Snippet } from 'svelte';\n\timport type { HTMLAttributes } from 'svelte/elements';\n\timport { restoreScroll, resetScroll } from './handleScroll';\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tshowMap = $bindable(),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: {\n\t\tref?: HTMLDivElement | null;\n\t\tshowMap?: boolean;\n\t\tclass?: string;\n\t\tchildren?: Snippet;\n\t} & HTMLAttributes<HTMLDivElement> = $props();\n\n\tlet expanded = $state(true);\n\tconst maxTranslate = 0.7;\n\tconst minTranslate = 0;\n\tlet startY = 0;\n\tlet currentY = 0;\n\tlet isDragging = false;\n\tlet fromHandle = false;\n\tlet container: HTMLElement | null;\n\n\tonMount(() => {\n\t\tconst cleanup = restoreScroll(container!);\n\t\treturn cleanup;\n\t});\n\n\t$effect(() => resetScroll(container!));\n\n\tconst getScrollableElement = (element: Element): Element | null => {\n\t\twhile (element && element !== document.documentElement) {\n\t\t\tconst style = window.getComputedStyle(element);\n\t\t\tconst overflowY = style.overflowY;\n\t\t\tconst hasScrollableY =\n\t\t\t\toverflowY !== 'visible' &&\n\t\t\t\toverflowY !== 'hidden' &&\n\t\t\t\telement.scrollHeight > element.clientHeight;\n\n\t\t\tif (hasScrollableY) {\n\t\t\t\treturn element;\n\t\t\t}\n\n\t\t\telement = element.parentElement as HTMLElement;\n\t\t}\n\n\t\treturn null;\n\t};\n\n\tconst ontouchstart = (e: TouchEvent) => {\n\t\tconst target = !fromHandle ? (e.target as Element) : null;\n\t\tconst scrollableElement = target ? (getScrollableElement(target) as HTMLElement) : null;\n\t\tstartY = e.touches[0].clientY;\n\t\tisDragging = expanded ? (scrollableElement ? scrollableElement.scrollTop === 0 : true) : true;\n\t};\n\n\tconst ontouchmove = (e: TouchEvent) => {\n\t\tif (!isDragging) return;\n\t\tcurrentY = e.touches[0].clientY;\n\t\tconst delta = currentY - startY;\n\t\tconst baseTranslate = expanded ? 0 : window.innerHeight * maxTranslate;\n\t\tif ((expanded && delta < 0) || (!expanded && delta > 0)) return;\n\t\tshowMap = true;\n\t\tref!.style.transition = 'none';\n\t\tref!.style.transform = `translateY(${baseTranslate + delta}px)`;\n\t};\n\tconst ontouchend = (e: TouchEvent) => {\n\t\tif (!isDragging) return;\n\t\tisDragging = false;\n\t\tref!.style.transition = '';\n\t\tconst delta = e.changedTouches[0].clientY - startY;\n\t\tif ((70 < delta || (delta == 0 && fromHandle)) && expanded) {\n\t\t\tref!.style.transform = `translateY(${window.innerHeight * maxTranslate}px)`;\n\t\t\texpanded = false;\n\t\t} else if ((delta < -70 || (delta == 0 && fromHandle)) && !expanded) {\n\t\t\tref!.style.transform = `translateY(${minTranslate}px)`;\n\t\t\texpanded = true;\n\t\t} else {\n\t\t\tref!.style.transform = `translateY(${expanded ? minTranslate : window.innerHeight * maxTranslate}px)`;\n\t\t}\n\t\tfromHandle = false;\n\t};\n</script>\n\n<div\n\tbind:this={ref}\n\tclass={cn(\n\t\t'bg-card text-card-foreground rounded-xl pb-3 h-full border shadow max-w-full transition-transform duration-200 ease-out',\n\t\tclassName\n\t)}\n\t{...restProps}\n\t{ontouchstart}\n\t{ontouchmove}\n\t{ontouchend}\n>\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"mx-auto my-5 relative before:content-[''] before:absolute before:inset-[-20px] before:inset-x-[-50vw] flex items-center justify-center\"\n\t\tonclick={() => {\n\t\t\tshowMap = true;\n\t\t\tref!.style.transform = `translateY(${(expanded ? window.innerHeight : 0) * maxTranslate}px)`;\n\t\t\texpanded = !expanded;\n\t\t}}\n\t>\n\t\t<div\n\t\t\tclass=\"absolute transition-all duration-200\"\n\t\t\tclass:opacity-0={!expanded}\n\t\t\tclass:opacity-100={expanded}\n\t\t>\n\t\t\t<ChevronDown class=\"size-12 text-gray-300 scale-x-150\" strokeWidth={3.5} />\n\t\t</div>\n\t\t<div\n\t\t\tclass=\"w-14 h-2 bg-gray-300 rounded-full transition-all duration-200\"\n\t\t\tclass:opacity-0={expanded}\n\t\t\tclass:opacity-100={!expanded}\n\t\t></div>\n\t</button>\n\n\t<div bind:this={container} class:overflow-auto={expanded}>\n\t\t{@render children?.()}\n\t</div>\n</div>\n"
  },
  {
    "path": "ui/src/lib/map/GeoJSON.svelte",
    "content": "<script lang=\"ts\">\n\timport maplibregl, { type GeoJSONSourceSpecification } from 'maplibre-gl';\n\timport GeoJSON from 'geojson';\n\timport { getContext, onDestroy, setContext, type Snippet } from 'svelte';\n\n\tclass Props {\n\t\tid!: string;\n\t\tdata!: GeoJSON.GeoJSON;\n\t\tlineMetrics?: boolean;\n\t\tchildren!: Snippet;\n\t\toptions?: Omit<GeoJSONSourceSpecification, 'type' | 'data'>;\n\t}\n\n\tlet { id, data, lineMetrics = false, children, options }: Props = $props();\n\n\tlet ctx: { map: maplibregl.Map | null } = getContext('map');\n\n\tlet sourceId = $state<{ id: null | string }>({ id: null });\n\tsetContext('source', sourceId);\n\n\tconst updateSource = () => {\n\t\tif (!ctx.map || data == null) {\n\t\t\treturn;\n\t\t}\n\t\tconst src = ctx.map!.getSource(id) as maplibregl.GeoJSONSource | undefined;\n\t\tif (src) {\n\t\t\tsrc.setData(data);\n\t\t} else {\n\t\t\tctx.map!.addSource(id, {\n\t\t\t\ttype: 'geojson',\n\t\t\t\tlineMetrics,\n\t\t\t\tdata,\n\t\t\t\t...(options ?? {})\n\t\t\t});\n\t\t}\n\t\tsourceId.id = id;\n\t};\n\n\tlet initialized = false;\n\t$effect(() => {\n\t\tif (ctx.map && id && data) {\n\t\t\tupdateSource();\n\t\t\tif (!initialized) {\n\t\t\t\tctx.map!.on('styledata', updateSource);\n\t\t\t\tsourceId.id = id;\n\t\t\t\tinitialized = true;\n\t\t\t}\n\t\t}\n\t});\n\n\tonDestroy(() => {\n\t\tif (initialized) {\n\t\t\tctx.map?.off('styledata', updateSource);\n\t\t\tsourceId.id = null;\n\t\t\tconst src = ctx.map!.getSource(id);\n\t\t\tif (src) {\n\t\t\t\tctx.map?.removeSource(id);\n\t\t\t}\n\t\t}\n\t});\n</script>\n\n{@render children()}\n"
  },
  {
    "path": "ui/src/lib/map/Isochrones.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount } from 'svelte';\n\timport maplibregl from 'maplibre-gl';\n\timport type { CanvasSource, GeoJSONSource, LngLatBoundsLike, Map } from 'maplibre-gl';\n\timport type { GeoJSON } from 'geojson';\n\timport type { PrePostDirectMode } from '$lib/Modes';\n\timport {\n\t\tisCanvasLevel,\n\t\tisLess,\n\t\tminDisplayLevel,\n\t\ttype DisplayLevel,\n\t\ttype IsochronesOptions,\n\t\ttype IsochronesPos\n\t} from '$lib/map/IsochronesShared';\n\timport type { WorkerMessage } from '$lib/map/IsochronesWorker';\n\timport WebWorker from '$lib/map/IsochronesWorker.ts?worker';\n\n\ttype BoxCoordsType = [[number, number], [number, number], [number, number], [number, number]];\n\n\tlet {\n\t\tmap,\n\t\tbounds,\n\t\tisochronesData,\n\t\tstreetModes,\n\t\twheelchair,\n\t\tmaxAllTime,\n\t\tcircleResolution,\n\t\tactive,\n\t\toptions = $bindable()\n\t}: {\n\t\tmap: Map | undefined;\n\t\tbounds: LngLatBoundsLike | undefined;\n\t\tisochronesData: IsochronesPos[];\n\t\tstreetModes: PrePostDirectMode[];\n\t\twheelchair: boolean;\n\t\tmaxAllTime: number;\n\t\tcircleResolution: number | undefined;\n\t\tactive: boolean;\n\t\toptions: IsochronesOptions;\n\t} = $props();\n\n\tconst emptyGeometry: GeoJSON = { type: 'LineString', coordinates: [] };\n\t// Must all exist\n\tlet objects = $state<\n\t\t| {\n\t\t\t\tworker: Worker;\n\t\t\t\tcanvasLayer: 'isochrones-canvas';\n\t\t\t\tcirclesLayer: 'isochrones-circles';\n\t\t\t\tcanvasSource: CanvasSource;\n\t\t\t\tcirclesSource: GeoJSONSource;\n\t\t  }\n\t\t| undefined\n\t>(undefined);\n\tlet bestAvailableDisplayLevel = $state<DisplayLevel>('NONE');\n\n\tconst kilometersPerSecond = $derived(\n\t\t// Should match the speed used for routing\n\t\tstreetModes.includes('BIKE')\n\t\t\t? 0.0038 // 3.8 meters per second\n\t\t\t: wheelchair\n\t\t\t\t? 0.0008 // 0.8 meters per second\n\t\t\t\t: 0.0012 // 1.2 meters per second\n\t);\n\tconst boundingBox = $derived(\n\t\tmaplibregl.LngLatBounds.convert(\n\t\t\tbounds ?? [\n\t\t\t\t[0, 0],\n\t\t\t\t[1, 1]\n\t\t\t]\n\t\t)\n\t);\n\tconst boxCoords: BoxCoordsType = $derived([\n\t\t[boundingBox._sw.lng, boundingBox._ne.lat],\n\t\t[boundingBox._ne.lng, boundingBox._ne.lat],\n\t\t[boundingBox._ne.lng, boundingBox._sw.lat],\n\t\t[boundingBox._sw.lng, boundingBox._sw.lat]\n\t]);\n\n\tlet lastData: IsochronesPos[] = [];\n\tlet lastMaxAllTime: number = 0;\n\tlet lastSpeed: number = 0;\n\tlet dataIndex = 0;\n\n\tonMount(() => {\n\t\tlastMaxAllTime = maxAllTime;\n\t\tlastSpeed = kilometersPerSecond;\n\t});\n\n\t// Setup objects\n\t$effect(() => {\n\t\tif (!map || !active || objects !== undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Create sources, layers and canvases\n\t\tconst canvasLayer = 'isochrones-canvas';\n\t\tconst circlesLayer = 'isochrones-circles';\n\n\t\tlet canvas = document.createElement('canvas');\n\t\tif (canvas === undefined) {\n\t\t\tconsole.log('Canvas not supported');\n\t\t\treturn;\n\t\t}\n\t\tlet offscreenCanvas = canvas.transferControlToOffscreen();\n\n\t\tmap.addSource(canvasLayer, {\n\t\t\ttype: 'canvas',\n\t\t\tcanvas,\n\t\t\tcoordinates: boxCoords\n\t\t});\n\t\tmap.addLayer({\n\t\t\tid: canvasLayer,\n\t\t\ttype: 'raster',\n\t\t\tsource: canvasLayer,\n\t\t\tpaint: {\n\t\t\t\t'raster-opacity': options.opacity / 1000\n\t\t\t}\n\t\t});\n\t\tconst canvasSource = map.getSource(canvasLayer) as CanvasSource;\n\n\t\tmap.addSource(circlesLayer, {\n\t\t\ttype: 'geojson',\n\t\t\tdata: emptyGeometry\n\t\t});\n\t\tmap.addLayer({\n\t\t\tid: circlesLayer,\n\t\t\ttype: 'fill',\n\t\t\tsource: circlesLayer,\n\t\t\tpaint: {\n\t\t\t\t'fill-color': options.color,\n\t\t\t\t'fill-opacity': options.opacity / 1000\n\t\t\t}\n\t\t});\n\t\tconst circlesSource = map.getSource(circlesLayer) as GeoJSONSource;\n\n\t\t// Setup worker\n\t\tconst worker = new WebWorker();\n\n\t\tworker.onmessage = (event: { data: WorkerMessage }) => {\n\t\t\tconst method = event.data.method;\n\t\t\tswitch (method) {\n\t\t\t\tcase 'update-display-level':\n\t\t\t\t\t{\n\t\t\t\t\t\tconst index = event.data.index;\n\t\t\t\t\t\tif (index < dataIndex) {\n\t\t\t\t\t\t\tconsole.log(`Got stale index from worker (Got ${index}, expected ${dataIndex})`);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst level: DisplayLevel = event.data.level;\n\t\t\t\t\t\tif (level == 'GEOMETRY_CIRCLES' && objects) {\n\t\t\t\t\t\t\tobjects.circlesSource.setData(event.data.geometry ?? emptyGeometry);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isLess(bestAvailableDisplayLevel, level)) {\n\t\t\t\t\t\t\tbestAvailableDisplayLevel = level;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tconsole.log(`Unknown method '${method}'`);\n\t\t\t}\n\t\t};\n\n\t\tworker.postMessage(\n\t\t\t{\n\t\t\t\tmethod: 'set-canvas',\n\t\t\t\tcanvas: offscreenCanvas\n\t\t\t},\n\t\t\t[offscreenCanvas]\n\t\t);\n\n\t\t// Store references\n\t\tobjects = {\n\t\t\tworker,\n\t\t\tcanvasLayer,\n\t\t\tcirclesLayer,\n\t\t\tcanvasSource,\n\t\t\tcirclesSource\n\t\t};\n\t});\n\n\t$effect(() => {\n\t\tif (!active || options.status == 'FAILED' || objects === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\t// isochronesData and lastData might both be empty, but have different references\n\t\tif (\n\t\t\t((lastData.length != 0 || isochronesData.length != 0) && lastData != isochronesData) ||\n\t\t\tlastMaxAllTime != maxAllTime ||\n\t\t\tlastSpeed != kilometersPerSecond\n\t\t) {\n\t\t\tobjects.worker.postMessage({\n\t\t\t\tmethod: 'update-data',\n\t\t\t\tindex: ++dataIndex,\n\t\t\t\tdata: $state.snapshot(isochronesData),\n\t\t\t\tkilometersPerSecond: $state.snapshot(kilometersPerSecond),\n\t\t\t\tmaxSeconds: $state.snapshot(maxAllTime),\n\t\t\t\tcircleResolution\n\t\t\t});\n\n\t\t\tlastData = isochronesData;\n\t\t\tlastMaxAllTime = maxAllTime;\n\t\t\tlastSpeed = kilometersPerSecond;\n\n\t\t\tbestAvailableDisplayLevel = 'NONE';\n\t\t\tobjects.circlesSource.setData(emptyGeometry);\n\t\t}\n\n\t\tobjects.worker.postMessage({\n\t\t\tmethod: 'set-max-display-level',\n\t\t\tdisplayLevel: options.displayLevel\n\t\t});\n\t});\n\n\t$effect(() => {\n\t\tif (!map || objects === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tmap.setLayoutProperty(\n\t\t\tobjects.canvasLayer,\n\t\t\t'visibility',\n\t\t\tactive && isCanvasLevel(currentDisplayLevel) ? 'visible' : 'none'\n\t\t);\n\t\tmap.setLayoutProperty(\n\t\t\tobjects.circlesLayer,\n\t\t\t'visibility',\n\t\t\tactive && currentDisplayLevel == 'GEOMETRY_CIRCLES' ? 'visible' : 'none'\n\t\t);\n\t});\n\n\t$effect(() => {\n\t\tif (!map || objects === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tmap.setPaintProperty(objects.canvasLayer, 'raster-opacity', options.opacity / 1000);\n\t\tmap.setPaintProperty(objects.circlesLayer, 'fill-opacity', options.opacity / 1000);\n\t});\n\n\t$effect(() => {\n\t\tif (!map || objects === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tmap.setPaintProperty(objects.circlesLayer, 'fill-color', options.color);\n\t});\n\n\tlet currentDisplayLevel = $derived.by<DisplayLevel>(() => {\n\t\tif (!map || !active || objects === undefined) {\n\t\t\treturn 'NONE';\n\t\t}\n\n\t\tconst nextLevel = minDisplayLevel(options.displayLevel, bestAvailableDisplayLevel);\n\n\t\tif (isCanvasLevel(nextLevel)) {\n\t\t\tobjects.canvasSource.setCoordinates(boxCoords);\n\n\t\t\tconst dimensions = map._containerDimensions();\n\n\t\t\tobjects.worker.postMessage({\n\t\t\t\tmethod: 'render-canvas',\n\t\t\t\tlevel: nextLevel,\n\t\t\t\tboundingBox: $state.snapshot(boundingBox),\n\t\t\t\tdimensions,\n\t\t\t\tcolor: options.color\n\t\t\t});\n\t\t}\n\n\t\treturn nextLevel;\n\t});\n\t$effect(() => {\n\t\toptions.status =\n\t\t\tisochronesData.length == 0\n\t\t\t\t? 'EMPTY'\n\t\t\t\t: currentDisplayLevel == 'NONE' || currentDisplayLevel == options.displayLevel\n\t\t\t\t\t? 'DONE'\n\t\t\t\t\t: 'WORKING';\n\t});\n</script>\n"
  },
  {
    "path": "ui/src/lib/map/IsochronesShapeWorker.ts",
    "content": "import { circle } from '@turf/circle';\nimport { destination } from '@turf/destination';\nimport { featureCollection, point } from '@turf/helpers';\nimport { union } from '@turf/union';\nimport {\n\tisLess,\n\ttype DisplayLevel,\n\ttype Geometry,\n\ttype IsochronesPos\n} from '$lib/map/IsochronesShared';\ntype LngLat = {\n\tlng: number;\n\tlat: number;\n};\ntype LngLatBounds = {\n\t_ne: LngLat;\n\t_sw: LngLat;\n};\nexport type UpdateMessage =\n\t| { level: 'OVERLAY_RECTS'; data: LngLatBounds[] }\n\t| { level: 'OVERLAY_CIRCLES'; data: CircleType[] }\n\t| { level: 'GEOMETRY_CIRCLES'; data: Geometry | undefined };\nexport type ShapeMessage = { method: 'update-shape'; index: number } & UpdateMessage;\ntype RectType = { rect: LngLatBounds; distance: number; data: IsochronesPos };\ntype CircleType = ReturnType<typeof circle>;\n\nlet dataIndex = 0;\nlet data: IsochronesPos[] | undefined = undefined;\nlet rects: RectType[] | undefined = undefined;\nlet circles: CircleType[] | undefined = undefined;\nlet circleGeometry: Geometry | undefined = undefined;\nlet highestComputedLevel: DisplayLevel = 'NONE';\nlet maxLevel: DisplayLevel = 'NONE';\nlet working = false;\nlet maxDistance = (_: IsochronesPos) => 0;\nlet circleResolution: number = 64; // default 'steps' for @turf/circle\n\nself.onmessage = async function (event) {\n\tconst method = event.data.method;\n\tif (method == 'set-data') {\n\t\tresetState(event.data.index);\n\t\tdata = event.data.data;\n\t\tconst kilometersPerSecond = event.data.kilometersPerSecond;\n\t\tconst maxSeconds = event.data.maxSeconds;\n\t\tmaxDistance = getMaxDistanceFunction(kilometersPerSecond, maxSeconds);\n\t\tif (event.data.circleResolution && event.data.circleResolution > 2) {\n\t\t\tcircleResolution = event.data.circleResolution;\n\t\t}\n\t} else if (method == 'set-max-level') {\n\t\tmaxLevel = event.data.level;\n\t\tcreateShapes();\n\t}\n};\n\nfunction resetState(index: number) {\n\tdataIndex = index;\n\trects = undefined;\n\tcircles = undefined;\n\tcircleGeometry = undefined;\n\thighestComputedLevel = 'NONE';\n\tmaxLevel = 'NONE';\n\tmaxDistance = (_: IsochronesPos) => 0;\n}\n\nfunction getMaxDistanceFunction(kilometersPerSecond: number, maxSeconds: number) {\n\treturn (pos: IsochronesPos) => Math.min(pos.seconds, maxSeconds) * kilometersPerSecond;\n}\n\nasync function createShapes() {\n\tif (working || !isLess(highestComputedLevel, maxLevel)) {\n\t\treturn;\n\t}\n\tworking = true;\n\tconst workingIndex = dataIndex;\n\tconst isStale = () => workingIndex != dataIndex;\n\tswitch (highestComputedLevel) {\n\t\tcase 'NONE':\n\t\t\tawait createRects().then(async (allRects: RectType[]) => {\n\t\t\t\tif (isStale()) {\n\t\t\t\t\tconsole.log('Index got stale while computing rects');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\trects = allRects;\n\t\t\t\thighestComputedLevel = 'OVERLAY_RECTS';\n\t\t\t\tself.postMessage({\n\t\t\t\t\tmethod: 'update-shape',\n\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\tlevel: 'OVERLAY_RECTS',\n\t\t\t\t\tdata: rects.map((r) => r.rect)\n\t\t\t\t} as ShapeMessage);\n\t\t\t\tawait filterNotContainedRects(allRects).then((notContainedRects: RectType[]) => {\n\t\t\t\t\tif (isStale()) {\n\t\t\t\t\t\tconsole.log('Index got stale deleting covered rects');\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trects = notContainedRects;\n\t\t\t\t\tself.postMessage({\n\t\t\t\t\t\tmethod: 'update-shape',\n\t\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\t\tlevel: 'OVERLAY_RECTS',\n\t\t\t\t\t\tdata: rects.map((r) => r.rect)\n\t\t\t\t\t} as ShapeMessage);\n\t\t\t\t});\n\t\t\t});\n\t\t\tbreak;\n\t\tcase 'OVERLAY_RECTS':\n\t\t\tawait createCircles().then((allCircles: CircleType[]) => {\n\t\t\t\tif (isStale()) {\n\t\t\t\t\tconsole.log('Index got stale while computing circles');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcircles = allCircles;\n\t\t\t\thighestComputedLevel = 'OVERLAY_CIRCLES';\n\t\t\t\tself.postMessage({\n\t\t\t\t\tmethod: 'update-shape',\n\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\tlevel: 'OVERLAY_CIRCLES',\n\t\t\t\t\tdata: circles\n\t\t\t\t} as ShapeMessage);\n\t\t\t});\n\t\t\tbreak;\n\t\tcase 'OVERLAY_CIRCLES':\n\t\t\tawait createUnion().then((geometry: Geometry | undefined) => {\n\t\t\t\tif (isStale()) {\n\t\t\t\t\tconsole.log('Index got stale while computing geometry');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcircleGeometry = geometry;\n\t\t\t\thighestComputedLevel = 'GEOMETRY_CIRCLES';\n\t\t\t\tself.postMessage({\n\t\t\t\t\tmethod: 'update-shape',\n\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\tlevel: 'GEOMETRY_CIRCLES',\n\t\t\t\t\tdata: circleGeometry\n\t\t\t\t} as ShapeMessage);\n\t\t\t});\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tconsole.log(`Unexpected level '${highestComputedLevel}'`);\n\t}\n\tworking = false;\n\tcreateShapes();\n}\n\nasync function createRects() {\n\tif (data === undefined) {\n\t\treturn [];\n\t}\n\tconst promises = data.map(async (pos: IsochronesPos) => {\n\t\tconst center = point([pos.lng, pos.lat]);\n\t\tconst r = maxDistance(pos);\n\t\tconst north = destination(center, r, 0, { units: 'kilometers' });\n\t\tconst east = destination(center, r, 90, { units: 'kilometers' });\n\t\tconst south = destination(center, r, 180, { units: 'kilometers' });\n\t\tconst west = destination(center, r, -90, { units: 'kilometers' });\n\t\treturn {\n\t\t\trect: {\n\t\t\t\t_sw: { lng: west.geometry.coordinates[0], lat: south.geometry.coordinates[1] } as LngLat,\n\t\t\t\t_ne: { lng: east.geometry.coordinates[0], lat: north.geometry.coordinates[1] } as LngLat\n\t\t\t} as LngLatBounds,\n\t\t\tdistance: r,\n\t\t\tdata: pos\n\t\t};\n\t});\n\treturn await Promise.all(promises);\n}\n\nfunction contains(larger: RectType, smaller: RectType): boolean {\n\tconst r1 = larger.rect;\n\tconst r2 = smaller.rect;\n\treturn (\n\t\tr1._sw.lat <= r2._sw.lat &&\n\t\tr1._sw.lng <= r2._sw.lng &&\n\t\tr1._ne.lat >= r2._ne.lat &&\n\t\tr1._ne.lng >= r2._ne.lng\n\t);\n}\n\nasync function filterNotContainedRects(allRects: RectType[]) {\n\t// Remove all rects, that are completely contained in at least one other\n\t// Sort by distance, descending\n\tallRects.sort((a: RectType, b: RectType) => b.distance - a.distance);\n\tconst isCoveredPromises = allRects.map(async (box: RectType, index: number) =>\n\t\tallRects.slice(0, index).some((b: RectType) => contains(b, box))\n\t);\n\tconst isCovered = await Promise.all(isCoveredPromises);\n\tconst visibleBoxes = allRects.filter((_: RectType, index: number) => !isCovered[index]);\n\treturn visibleBoxes;\n}\n\nasync function createCircles() {\n\tif (rects === undefined) {\n\t\treturn [];\n\t}\n\tconst promises = rects.map(async (rect: RectType) => {\n\t\tconst c = circle([rect.data.lng, rect.data.lat], rect.distance, {\n\t\t\tsteps: circleResolution,\n\t\t\tunits: 'kilometers'\n\t\t});\n\t\t// bbox extent in [minX, minY, maxX, maxY] order\n\t\tc.bbox = [rect.rect._sw.lng, rect.rect._sw.lat, rect.rect._ne.lng, rect.rect._ne.lat];\n\t\treturn c;\n\t});\n\treturn await Promise.all(promises);\n}\n\n// Implementation based on https://stackoverflow.com/a/75982694\n// Create union for smaller polygons first\n// Using a pipe like approach should place larger polygons at the end,\n// reducing the number of expensive computations\n\nasync function createUnion() {\n\tif (circles === undefined) {\n\t\treturn undefined;\n\t}\n\tconst queue: Geometry[] = circles.map((c: CircleType) => c);\n\twhile (queue.length > 1) {\n\t\tconst a: Geometry = queue.shift()!;\n\t\tconst b: Geometry = queue.shift()!;\n\t\tconst u: Geometry | null = union(featureCollection([a, b]));\n\t\tif (u) {\n\t\t\tqueue.push(u);\n\t\t}\n\t}\n\treturn queue.length == 1 ? queue[0] : undefined;\n}\n"
  },
  {
    "path": "ui/src/lib/map/IsochronesShared.ts",
    "content": "import type { Feature, GeoJsonProperties, MultiPolygon, Polygon } from 'geojson';\nconst DisplayLevels = ['NONE', 'OVERLAY_RECTS', 'OVERLAY_CIRCLES', 'GEOMETRY_CIRCLES'] as const;\n\nexport type DisplayLevel = (typeof DisplayLevels)[number];\nexport type StatusLevel = 'WORKING' | 'DONE' | 'EMPTY' | 'FAILED';\nexport type Geometry = Feature<Polygon | MultiPolygon, GeoJsonProperties>;\n\nexport interface IsochronesOptions {\n\tdisplayLevel: DisplayLevel;\n\tcolor: string;\n\topacity: number;\n\tstatus: StatusLevel;\n\terrorMessage: string | undefined;\n\terrorCode: number | undefined;\n}\nexport interface IsochronesPos {\n\tlat: number;\n\tlng: number;\n\tseconds: number;\n}\n\nexport const isLess = (a: DisplayLevel, b: DisplayLevel) =>\n\tDisplayLevels.indexOf(a) < DisplayLevels.indexOf(b);\nexport const minDisplayLevel = (a: DisplayLevel, b: DisplayLevel) => (isLess(a, b) ? a : b);\n\nexport const isCanvasLevel = (a: DisplayLevel) => a == 'OVERLAY_RECTS' || a == 'OVERLAY_CIRCLES';\n"
  },
  {
    "path": "ui/src/lib/map/IsochronesWorker.ts",
    "content": "import type { circle } from '@turf/circle';\nimport type { LngLatBounds } from 'maplibre-gl';\nimport type { Position } from 'geojson';\nimport type { ShapeMessage, UpdateMessage } from '$lib/map/IsochronesShapeWorker';\nimport type { DisplayLevel, Geometry } from '$lib/map/IsochronesShared';\nimport ShapeWorker from '$lib/map/IsochronesShapeWorker.ts?worker';\n\nexport type WorkerMessage = {\n\tmethod: 'update-display-level';\n\tlevel: DisplayLevel;\n\tgeometry?: Geometry | undefined;\n\tindex: number;\n};\ntype CircleType = ReturnType<typeof circle>;\n\nlet canvas: OffscreenCanvas | undefined = undefined;\nlet dataIndex = 0;\nlet shapeWorker: Worker | undefined = undefined;\nlet rects: LngLatBounds[] | undefined = undefined;\nlet circles: CircleType[] | undefined = undefined;\n\nself.onmessage = async function (event) {\n\tconst method = event.data.method;\n\tif (method == 'set-canvas') {\n\t\tcanvas = event.data.canvas;\n\t} else if (method == 'update-data') {\n\t\tconst index: number = event.data.index;\n\t\tconst isochronesData = event.data.data;\n\t\tconst kilometersPerSecond: number = event.data.kilometersPerSecond;\n\t\tconst maxSeconds: number = event.data.maxSeconds;\n\t\tconst circleResolution: number = event.data.circleResolution;\n\t\tdataIndex = index;\n\t\trects = undefined;\n\t\tcircles = undefined;\n\t\tconst worker = createWorker();\n\t\tworker.postMessage({\n\t\t\tmethod: 'set-data',\n\t\t\tindex: dataIndex,\n\t\t\tdata: isochronesData,\n\t\t\tkilometersPerSecond,\n\t\t\tmaxSeconds,\n\t\t\tcircleResolution\n\t\t});\n\t} else if (method == 'set-max-display-level') {\n\t\tif (shapeWorker !== undefined) {\n\t\t\tconst level: DisplayLevel = event.data.displayLevel;\n\t\t\tshapeWorker.postMessage({ method: 'set-max-level', level: level });\n\t\t}\n\t} else if (method == 'render-canvas') {\n\t\tif (!canvas) {\n\t\t\treturn;\n\t\t}\n\t\tconst boundingBox: LngLatBounds = event.data.boundingBox;\n\t\tconst color: string = event.data.color;\n\t\tconst dimensions: [number, number] = event.data.dimensions;\n\t\tconst level: DisplayLevel = event.data.level;\n\t\tcanvas.width = dimensions[0];\n\t\tcanvas.height = dimensions[1];\n\t\tconst ctx = canvas.getContext('2d');\n\t\tif (!ctx) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst transform = getTransformer(boundingBox, dimensions);\n\n\t\tctx.fillStyle = color;\n\t\tctx.clearRect(0, 0, dimensions[0], dimensions[1]);\n\n\t\tif (level == 'OVERLAY_RECTS') {\n\t\t\tdrawRects(ctx, transform);\n\t\t} else if (level == 'OVERLAY_CIRCLES') {\n\t\t\tconst isVisible = getIsVisible(boundingBox);\n\t\t\tdrawCircles(ctx, transform, isVisible);\n\t\t} else {\n\t\t\tconsole.log(`Cannot render level ${level}`);\n\t\t}\n\t}\n};\n\nfunction getTransformer(boundingBox: LngLatBounds, dimensions: [number, number]) {\n\treturn (pos: Position) => {\n\t\tconst x = Math.round(\n\t\t\t((pos[0] - boundingBox._sw.lng) / (boundingBox._ne.lng - boundingBox._sw.lng)) * dimensions[0]\n\t\t);\n\t\tconst y = Math.round(\n\t\t\t((boundingBox._ne.lat - pos[1]) / (boundingBox._ne.lat - boundingBox._sw.lat)) * dimensions[1]\n\t\t);\n\t\treturn [x, y];\n\t};\n}\n\nfunction getIsVisible(boundingBox: LngLatBounds) {\n\treturn (circle: CircleType) => {\n\t\tif (!circle.bbox) {\n\t\t\treturn false;\n\t\t}\n\t\tconst b = circle.bbox; // [minX, minY, maxX, maxY]\n\t\treturn (\n\t\t\tboundingBox._sw.lat <= b[3] &&\n\t\t\tb[1] <= boundingBox._ne.lat &&\n\t\t\tboundingBox._sw.lng <= b[2] &&\n\t\t\tb[0] <= boundingBox._ne.lat\n\t\t);\n\t};\n}\n\nfunction drawRects(ctx: OffscreenCanvasRenderingContext2D, transform: (p: Position) => Position) {\n\tif (rects === undefined) {\n\t\treturn;\n\t}\n\trects.forEach((rect: LngLatBounds) => {\n\t\tctx.save(); // Store canvas state\n\n\t\tconst min = transform([rect._sw.lng, rect._sw.lat]);\n\t\tconst max = transform([rect._ne.lng, rect._ne.lat]);\n\t\tconst diffX = max[0] - min[0];\n\t\tconst diffY = max[1] - min[1];\n\t\tctx.fillRect(min[0], min[1], diffX + 1, diffY + 1);\n\t\t// Restore previous state on top\n\t\tctx.restore();\n\t});\n}\n\nfunction drawCircles(\n\tctx: OffscreenCanvasRenderingContext2D,\n\ttransform: (_: Position) => Position,\n\tisVisible: (_: CircleType) => boolean\n) {\n\tif (circles === undefined) {\n\t\treturn;\n\t}\n\tcircles.filter(isVisible).forEach((circle: CircleType) => {\n\t\tctx.save(); // Store canvas state\n\n\t\tconst b = circle.bbox!; // Existence checked in filter()\n\t\tconst min = transform([b[0], b[1]]);\n\t\tconst max = transform([b[2], b[3]]);\n\t\tconst diffX = max[0] - min[0];\n\t\tconst diffY = max[1] - min[1];\n\n\t\tif (diffX < 2 && diffY < 2) {\n\t\t\t// Draw small rect\n\t\t\tctx.fillRect(min[0], min[1], diffX + 1, diffY + 1);\n\t\t} else {\n\t\t\t// Clip circle\n\t\t\tctx.beginPath();\n\t\t\tconst coords = circle.geometry.coordinates[0];\n\t\t\tconst start = transform(coords[0]);\n\t\t\tctx.moveTo(start[0], start[1]);\n\t\t\tfor (let i = 0; i < coords.length; ++i) {\n\t\t\t\tconst pos = transform(coords[i]);\n\t\t\t\tctx.lineTo(pos[0], pos[1]);\n\t\t\t}\n\t\t\tctx.clip();\n\n\t\t\t// Fill bounding box, clipped to circle\n\t\t\tctx.fillRect(min[0], min[1], diffX + 1, diffY + 1);\n\t\t}\n\n\t\t// Restore previous state on top\n\t\tctx.restore();\n\t});\n}\n\nfunction createWorker() {\n\tshapeWorker?.terminate();\n\n\tshapeWorker = new ShapeWorker();\n\n\tshapeWorker.onmessage = (event: { data: ShapeMessage }) => {\n\t\tconst method = event.data.method;\n\t\tswitch (method) {\n\t\t\tcase 'update-shape':\n\t\t\t\t{\n\t\t\t\t\tconst index = event.data.index;\n\t\t\t\t\tif (index < dataIndex) {\n\t\t\t\t\t\tconsole.log(`Got stale index from shape worker (Got ${index}, expected ${dataIndex})`);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst msg: UpdateMessage = event.data;\n\t\t\t\t\tswitch (msg.level) {\n\t\t\t\t\t\tcase 'OVERLAY_RECTS':\n\t\t\t\t\t\t\trects = msg.data as maplibregl.LngLatBounds[];\n\t\t\t\t\t\t\tself.postMessage({\n\t\t\t\t\t\t\t\tmethod: 'update-display-level',\n\t\t\t\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\t\t\t\tlevel: msg.level\n\t\t\t\t\t\t\t} as WorkerMessage);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'OVERLAY_CIRCLES':\n\t\t\t\t\t\t\tcircles = msg.data;\n\t\t\t\t\t\t\tself.postMessage({\n\t\t\t\t\t\t\t\tmethod: 'update-display-level',\n\t\t\t\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\t\t\t\tlevel: msg.level\n\t\t\t\t\t\t\t} as WorkerMessage);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'GEOMETRY_CIRCLES':\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tconst geometry = msg.data;\n\t\t\t\t\t\t\t\tself.postMessage({\n\t\t\t\t\t\t\t\t\tmethod: 'update-display-level',\n\t\t\t\t\t\t\t\t\tindex: dataIndex,\n\t\t\t\t\t\t\t\t\tlevel: msg.level,\n\t\t\t\t\t\t\t\t\tgeometry: geometry\n\t\t\t\t\t\t\t\t} as WorkerMessage);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tconsole.log(`Unknown message '${msg}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tconsole.log(`Unknown method '${method}'`);\n\t\t}\n\t};\n\treturn shapeWorker;\n}\n"
  },
  {
    "path": "ui/src/lib/map/Layer.svelte",
    "content": "<script lang=\"ts\">\n\timport maplibregl from 'maplibre-gl';\n\timport { onDestroy, getContext, setContext, type Snippet } from 'svelte';\n\timport type { MapMouseEvent, MapGeoJSONFeature } from 'maplibre-gl';\n\n\ttype ClickHandler = (\n\t\te: MapMouseEvent & { features?: MapGeoJSONFeature[] },\n\t\tmap: maplibregl.Map\n\t) => void;\n\n\tlet {\n\t\tid,\n\t\ttype,\n\t\tfilter,\n\t\tlayout,\n\t\tpaint,\n\t\tbeforeLayerId = 'road-ref-shield',\n\t\tonclick,\n\t\tonmousemove,\n\t\tonmouseenter,\n\t\tonmouseleave,\n\t\tchildren\n\t}: {\n\t\tid: string;\n\t\ttype: 'symbol' | 'fill' | 'line' | 'circle';\n\t\tfilter: maplibregl.FilterSpecification;\n\t\tlayout: Object; // eslint-disable-line\n\t\tpaint: Object; // eslint-disable-line\n\t\tbeforeLayerId?: string;\n\t\tonclick?: ClickHandler;\n\t\tonmousemove?: ClickHandler;\n\t\tonmouseenter?: ClickHandler;\n\t\tonmouseleave?: ClickHandler;\n\t\tchildren?: Snippet;\n\t} = $props();\n\n\ttype PendingState = {\n\t\tmoves: Map<string, string>;\n\t\tframe: number | null;\n\t};\n\n\tconst pendingMoves = new WeakMap<maplibregl.Map, PendingState>();\n\n\tconst ensurePendingState = (map: maplibregl.Map): PendingState => {\n\t\tlet state = pendingMoves.get(map);\n\t\tif (!state) {\n\t\t\tstate = { moves: new Map(), frame: null };\n\t\t\tpendingMoves.set(map, state);\n\t\t}\n\t\treturn state;\n\t};\n\n\tfunction click(e: MapMouseEvent & { features?: MapGeoJSONFeature[] }) {\n\t\tif (onclick) {\n\t\t\tonclick(e, ctx.map!);\n\t\t}\n\t}\n\n\tfunction mousemove(e: MapMouseEvent & { features?: MapGeoJSONFeature[] }) {\n\t\tif (onmousemove) {\n\t\t\tonmousemove(e, ctx.map!);\n\t\t}\n\t}\n\n\tfunction mouseenter(e: MapMouseEvent & { features?: MapGeoJSONFeature[] }) {\n\t\tif (onmouseenter) {\n\t\t\tonmouseenter(e, ctx.map!);\n\t\t}\n\t}\n\n\tfunction mouseleave(e: MapMouseEvent & { features?: MapGeoJSONFeature[] }) {\n\t\tif (onmouseleave) {\n\t\t\tonmouseleave(e, ctx.map!);\n\t\t}\n\t}\n\n\tconst processPendingMoves = (map: maplibregl.Map) => {\n\t\tconst state = pendingMoves.get(map);\n\t\tif (!state) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const [layerId, beforeId] of Array.from(state.moves.entries())) {\n\t\t\tif (!map.getLayer(layerId)) {\n\t\t\t\tstate.moves.delete(layerId);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!map.getLayer(beforeId)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tmap.moveLayer(layerId, beforeId);\n\t\t\tstate.moves.delete(layerId);\n\t\t}\n\t\tif (state.moves.size === 0) {\n\t\t\tif (state.frame !== null) {\n\t\t\t\tcancelAnimationFrame(state.frame);\n\t\t\t}\n\t\t\tpendingMoves.delete(map);\n\t\t}\n\t};\n\n\tconst scheduleMoveLayer = (\n\t\tmap: maplibregl.Map,\n\t\tlayerId: string,\n\t\tbeforeId: string | undefined\n\t) => {\n\t\tif (!beforeId || beforeId === layerId) {\n\t\t\treturn;\n\t\t}\n\t\tconst state = ensurePendingState(map);\n\t\tstate.moves.set(layerId, beforeId);\n\t\tif (state.frame !== null) {\n\t\t\treturn;\n\t\t}\n\t\tstate.frame = requestAnimationFrame(() => {\n\t\t\tstate.frame = null;\n\t\t\tprocessPendingMoves(map);\n\t\t});\n\t};\n\n\tconst clearPendingForLayer = (map: maplibregl.Map | null | undefined, layerId: string) => {\n\t\tif (!map) {\n\t\t\treturn;\n\t\t}\n\t\tconst state = pendingMoves.get(map);\n\t\tif (!state) {\n\t\t\treturn;\n\t\t}\n\t\tstate.moves.delete(layerId);\n\t\tif (state.moves.size === 0) {\n\t\t\tif (state.frame !== null) {\n\t\t\t\tcancelAnimationFrame(state.frame);\n\t\t\t}\n\t\t\tpendingMoves.delete(map);\n\t\t}\n\t};\n\n\tlet layer = $state<{ id: null | string }>({ id });\n\tsetContext('layer', layer);\n\n\tlet ctx: { map: maplibregl.Map | null } = getContext('map'); // from Map component\n\tlet source: { id: string | null } = getContext('source'); // from GeoJSON component\n\n\tlet initialized = false;\n\tlet currFilter: maplibregl.FilterSpecification | undefined;\n\tlet currLayout: object | undefined;\n\tlet currPaint: object | undefined;\n\tlet currBefore: string | undefined;\n\n\tlet updateLayer = () => {\n\t\tconst map = ctx.map;\n\t\tconst l = map?.getLayer(id);\n\t\tif (!source.id) {\n\t\t\tif (l) {\n\t\t\t\tlayer.id = null;\n\t\t\t\tmap?.removeLayer(id);\n\t\t\t}\n\t\t\tclearPendingForLayer(map, id);\n\t\t\treturn;\n\t\t}\n\n\t\tif (l && filter == currFilter && layout == currLayout && paint == currPaint) {\n\t\t\tif (map) {\n\t\t\t\tprocessPendingMoves(map);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (!l) {\n\t\t\tconst before = beforeLayerId && map?.getLayer(beforeLayerId) ? beforeLayerId : undefined;\n\t\t\tmap!.addLayer(\n\t\t\t\t// @ts-expect-error not assignable\n\t\t\t\t{\n\t\t\t\t\tsource: source.id,\n\t\t\t\t\tid,\n\t\t\t\t\ttype,\n\t\t\t\t\tfilter,\n\t\t\t\t\tlayout,\n\t\t\t\t\tpaint\n\t\t\t\t},\n\t\t\t\tbefore\n\t\t\t);\n\t\t\tcurrFilter = filter;\n\t\t\tcurrLayout = layout;\n\t\t\tcurrPaint = paint;\n\t\t\tcurrBefore = beforeLayerId;\n\t\t\tlayer.id = id;\n\t\t\tif (beforeLayerId && !before) {\n\t\t\t\tscheduleMoveLayer(map!, id, beforeLayerId);\n\t\t\t}\n\t\t\tprocessPendingMoves(map!);\n\t\t\treturn;\n\t\t}\n\n\t\tif (currFilter != filter) {\n\t\t\tcurrFilter = filter;\n\t\t\tmap!.setFilter(id, filter);\n\t\t}\n\t\tif (currBefore !== beforeLayerId) {\n\t\t\tcurrBefore = beforeLayerId;\n\t\t\tif (beforeLayerId && map?.getLayer(beforeLayerId)) {\n\t\t\t\tmap!.moveLayer(id, beforeLayerId);\n\t\t\t} else if (beforeLayerId) {\n\t\t\t\tscheduleMoveLayer(map!, id, beforeLayerId);\n\t\t\t}\n\t\t}\n\t\tif (map) {\n\t\t\tprocessPendingMoves(map);\n\t\t}\n\t};\n\n\t$effect(() => {\n\t\tif (ctx.map && source.id) {\n\t\t\tif (!initialized) {\n\t\t\t\tif (onclick) {\n\t\t\t\t\tctx.map.on('click', id, click);\n\t\t\t\t}\n\t\t\t\tif (onmousemove) {\n\t\t\t\t\tctx.map.on('mousemove', id, mousemove);\n\t\t\t\t}\n\t\t\t\tif (onmouseenter) {\n\t\t\t\t\tctx.map.on('mouseenter', id, mouseenter);\n\t\t\t\t}\n\t\t\t\tif (onmouseleave) {\n\t\t\t\t\tctx.map.on('mouseleave', id, mouseleave);\n\t\t\t\t}\n\t\t\t\tctx.map.on('styledata', updateLayer);\n\t\t\t\tupdateLayer();\n\t\t\t\tinitialized = true;\n\t\t\t}\n\t\t\tprocessPendingMoves(ctx.map);\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (initialized) {\n\t\t\tupdateLayer();\n\t\t}\n\t});\n\n\tonDestroy(() => {\n\t\tconst l = ctx.map?.getLayer(id);\n\t\tctx.map?.off('styledata', updateLayer);\n\t\tif (onclick) {\n\t\t\tctx.map?.off('click', id, click);\n\t\t}\n\t\tif (onmousemove) {\n\t\t\tctx.map?.off('mousemove', id, mousemove);\n\t\t}\n\t\tif (onmouseenter) {\n\t\t\tctx.map?.off('mouseenter', id, mouseenter);\n\t\t}\n\t\tif (onmouseleave) {\n\t\t\tctx.map?.off('mouseleave', id, mouseleave);\n\t\t}\n\t\tif (l) {\n\t\t\tctx.map?.removeLayer(id);\n\t\t}\n\t\tclearPendingForLayer(ctx.map, id);\n\t});\n</script>\n\n{#if children}\n\t{@render children()}\n{/if}\n"
  },
  {
    "path": "ui/src/lib/map/Map.svelte",
    "content": "<script lang=\"ts\">\n\timport maplibregl from 'maplibre-gl';\n\timport { setContext, type Snippet } from 'svelte';\n\timport 'maplibre-gl/dist/maplibre-gl.css';\n\timport { createShield } from './shield';\n\timport { browser } from '$app/environment';\n\tlet {\n\t\tmap = $bindable(),\n\t\tzoom = $bindable(),\n\t\tbounds = $bindable(),\n\t\tcenter = $bindable(),\n\t\tstyle,\n\t\tattribution,\n\t\ttransformRequest,\n\t\tchildren,\n\t\tclass: className\n\t}: {\n\t\tmap?: maplibregl.Map;\n\t\tstyle: maplibregl.StyleSpecification | undefined;\n\t\tattribution: string | undefined | false;\n\t\ttransformRequest?: maplibregl.RequestTransformFunction;\n\t\tcenter: maplibregl.LngLatLike;\n\t\tbounds?: maplibregl.LngLatBoundsLike | undefined;\n\t\tzoom: number;\n\t\tchildren?: Snippet;\n\t\tclass: string;\n\t} = $props();\n\n\tlet el: HTMLElement | null = null;\n\tlet currStyle: maplibregl.StyleSpecification | undefined = style;\n\tlet ctx = $state<{ map: maplibregl.Map | undefined }>({ map: undefined });\n\tsetContext('map', ctx);\n\n\tconst updateStyle = () => {\n\t\tif (style != currStyle) {\n\t\t\tif (!ctx.map && el) {\n\t\t\t\tcreateMap(el);\n\t\t\t} else if (ctx.map) {\n\t\t\t\tctx.map.setStyle(style || null);\n\t\t\t}\n\t\t\tcurrStyle = style;\n\t\t}\n\t};\n\tconst createMap = (container: HTMLElement) => {\n\t\tif (!style) {\n\t\t\treturn;\n\t\t}\n\t\tlet tmp: maplibregl.Map;\n\t\ttry {\n\t\t\ttmp = new maplibregl.Map({\n\t\t\t\tcontainer,\n\t\t\t\tzoom,\n\t\t\t\tbounds,\n\t\t\t\tcenter,\n\t\t\t\tstyle,\n\t\t\t\tpitchWithRotate: false,\n\t\t\t\tfadeDuration: 0,\n\t\t\t\ttransformRequest,\n\t\t\t\tattributionControl:\n\t\t\t\t\tattribution === false || attribution === undefined\n\t\t\t\t\t\t? attribution\n\t\t\t\t\t\t: { customAttribution: attribution }\n\t\t\t});\n\t\t\ttmp.addImage(\n\t\t\t\t'shield',\n\t\t\t\t...createShield({\n\t\t\t\t\tfill: 'hsl(0, 0%, 98%)',\n\t\t\t\t\tstroke: 'hsl(0, 0%, 75%)'\n\t\t\t\t})\n\t\t\t);\n\n\t\t\ttmp.addImage(\n\t\t\t\t'shield-dark',\n\t\t\t\t...createShield({\n\t\t\t\t\tfill: 'hsl(0, 0%, 16%)',\n\t\t\t\t\tstroke: 'hsl(0, 0%, 30%)'\n\t\t\t\t})\n\t\t\t);\n\n\t\t\tconst scale = new maplibregl.ScaleControl({\n\t\t\t\tmaxWidth: 100,\n\t\t\t\tunit: 'metric'\n\t\t\t});\n\n\t\t\ttmp.addControl(scale, browser && window.innerWidth < 768 ? 'top-left' : 'bottom-left');\n\n\t\t\ttmp.on('load', () => {\n\t\t\t\tmap = tmp;\n\t\t\t\tctx.map = tmp;\n\t\t\t\tbounds = tmp.getBounds();\n\t\t\t\ttmp.on('moveend', () => {\n\t\t\t\t\tzoom = tmp.getZoom();\n\t\t\t\t\tcenter = tmp.getCenter();\n\t\t\t\t\tbounds = tmp.getBounds();\n\t\t\t\t});\n\t\t\t});\n\t\t} catch (e) {\n\t\t\tconsole.log(e);\n\t\t}\n\n\t\treturn {\n\t\t\tdestroy() {\n\t\t\t\ttmp?.remove();\n\t\t\t\tctx.map = undefined;\n\t\t\t}\n\t\t};\n\t};\n\n\t$effect(updateStyle);\n</script>\n\n<div use:createMap bind:this={el} class={className}>\n\t{#if children}\n\t\t{@render children()}\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/map/Marker.svelte",
    "content": "<script lang=\"ts\">\n\timport { posToLocation, type Location } from '$lib/Location';\n\timport maplibregl from 'maplibre-gl';\n\timport { getContext, onDestroy } from 'svelte';\n\n\tlet ctx: { map: maplibregl.Map | null } = getContext('map'); // from Map component\n\n\tlet {\n\t\tcolor,\n\t\tdraggable,\n\t\tlevel,\n\t\tlocation = $bindable(),\n\t\tmarker = $bindable()\n\t}: {\n\t\tcolor: string;\n\t\tdraggable: boolean;\n\t\tlevel?: number;\n\t\tlocation: Location;\n\t\tmarker?: maplibregl.Marker;\n\t} = $props();\n\n\tlet initialized = false;\n\n\t$effect(() => {\n\t\tif (ctx.map && location.match) {\n\t\t\tif (!initialized) {\n\t\t\t\tmarker = new maplibregl.Marker({\n\t\t\t\t\tdraggable,\n\t\t\t\t\tcolor\n\t\t\t\t})\n\t\t\t\t\t.setLngLat(location.match)\n\t\t\t\t\t.addTo(ctx.map);\n\t\t\t\tmarker.on('dragend', () => {\n\t\t\t\t\tif (marker && location.match) {\n\t\t\t\t\t\tlet x = posToLocation(marker.getLngLat(), level ?? 0);\n\t\t\t\t\t\tlocation = x;\n\t\t\t\t\t\tlocation.label = x.label;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tinitialized = true;\n\t\t\t}\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (marker && location && location.match && location.match.lat && location.match.lon) {\n\t\t\tmarker.setLngLat(location.match);\n\t\t}\n\t});\n\n\tonDestroy(() => {\n\t\tif (marker) {\n\t\t\tmarker.remove();\n\t\t\tmarker = undefined;\n\t\t}\n\t});\n</script>\n"
  },
  {
    "path": "ui/src/lib/map/Popup.svelte",
    "content": "<script lang=\"ts\">\n\timport maplibregl from 'maplibre-gl';\n\timport { getContext, onDestroy, type Snippet } from 'svelte';\n\n\ttype PopupSnapshot = {\n\t\tlngLat: maplibregl.LngLatLike;\n\t\tevent: maplibregl.MapMouseEvent;\n\t\tfeatures?: maplibregl.MapGeoJSONFeature[];\n\t};\n\n\ttype PopupController = {\n\t\topen?: (snapshot: PopupSnapshot) => void;\n\t\tclose?: () => void;\n\t\tgetSnapshot?: () => PopupSnapshot | null;\n\t\tonSnapshotChange?: (snapshot: PopupSnapshot | null) => void;\n\t};\n\n\tlet {\n\t\tchildren,\n\t\tclass: className,\n\t\ttrigger,\n\t\tcontroller\n\t}: {\n\t\tchildren?: Snippet<\n\t\t\t[maplibregl.MapMouseEvent, () => void, maplibregl.MapGeoJSONFeature[] | undefined]\n\t\t>;\n\t\tclass?: string;\n\t\ttrigger: 'click' | 'contextmenu';\n\t\tcontroller?: PopupController;\n\t} = $props();\n\n\tlet ctx: { map: maplibregl.Map | null } = getContext('map'); // from Map component\n\tlet layer: { id: string } | null = getContext('layer'); // from Layer component (optional)\n\n\tlet popupEl = $state<HTMLDivElement>();\n\tlet popup = $state<maplibregl.Popup>();\n\tlet event = $state.raw<maplibregl.MapMouseEvent>();\n\tlet features = $state.raw<maplibregl.MapGeoJSONFeature[]>();\n\n\tconst clearPopupState = () => {\n\t\tpopup = undefined;\n\t\tevent = undefined;\n\t\tfeatures = undefined;\n\t\tcontroller?.onSnapshotChange?.(null);\n\t};\n\n\tconst openPopup = (snapshot: PopupSnapshot) => {\n\t\tif (!ctx.map) {\n\t\t\treturn;\n\t\t}\n\t\tif (popup) {\n\t\t\tpopup.remove();\n\t\t}\n\t\tconst nextPopup = new maplibregl.Popup({\n\t\t\tanchor: 'top-left',\n\t\t\tcloseButton: false,\n\t\t\tmaxWidth: 'none'\n\t\t});\n\t\tnextPopup.on('close', () => {\n\t\t\tif (popup === nextPopup) {\n\t\t\t\tclearPopupState();\n\t\t\t}\n\t\t});\n\t\tnextPopup.setLngLat(snapshot.lngLat);\n\t\tnextPopup.addTo(ctx.map);\n\t\tpopup = nextPopup;\n\t\tevent = snapshot.event;\n\t\tfeatures = snapshot.features;\n\t\tcontroller?.onSnapshotChange?.(snapshot);\n\t};\n\n\tconst close = () => popup?.remove();\n\n\tconst onTrigger = (e: maplibregl.MapLayerMouseEvent) => {\n\t\topenPopup({ lngLat: e.lngLat, event: e, features: e.features });\n\t};\n\n\tconst onMouseEnter = () => {\n\t\tif (ctx.map) {\n\t\t\tctx.map.getCanvas().style.cursor = 'pointer';\n\t\t}\n\t};\n\n\tconst onMouseLeave = () => {\n\t\tif (ctx.map) {\n\t\t\tctx.map.getCanvas().style.cursor = '';\n\t\t}\n\t};\n\n\tlet initialized = false;\n\t$effect(() => {\n\t\tif (ctx.map) {\n\t\t\tif (!initialized) {\n\t\t\t\tif (layer) {\n\t\t\t\t\tctx.map.on(trigger, layer.id, onTrigger);\n\t\t\t\t\tctx.map.on('mouseenter', layer.id, onMouseEnter);\n\t\t\t\t\tctx.map.on('mouseleave', layer.id, onMouseLeave);\n\t\t\t\t} else {\n\t\t\t\t\tctx.map.on(trigger, onTrigger);\n\t\t\t\t}\n\t\t\t}\n\t\t\tinitialized = true;\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (popup && popupEl) {\n\t\t\tpopup.setDOMContent(popupEl);\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (controller) {\n\t\t\tcontroller.open = openPopup;\n\t\t\tcontroller.close = close;\n\t\t\tcontroller.getSnapshot = () => {\n\t\t\t\tif (!popup || !event) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tlngLat: popup.getLngLat(),\n\t\t\t\t\tevent,\n\t\t\t\t\tfeatures\n\t\t\t\t};\n\t\t\t};\n\t\t}\n\t});\n\n\tonDestroy(() => {\n\t\tif (popup) {\n\t\t\tpopup.remove();\n\t\t\tclearPopupState();\n\t\t}\n\t\tif (ctx.map && initialized) {\n\t\t\tif (layer) {\n\t\t\t\tctx.map.off(trigger, layer.id, onTrigger);\n\t\t\t\tctx.map.off('mouseenter', layer.id, onMouseEnter);\n\t\t\t\tctx.map.off('mouseleave', layer.id, onMouseLeave);\n\t\t\t} else {\n\t\t\t\tctx.map.off(trigger, onTrigger);\n\t\t\t}\n\t\t}\n\t});\n</script>\n\n{#if popup && event}\n\t<div bind:this={popupEl} class={className}>\n\t\t{#if children}\n\t\t\t{@render children(event, close, features)}\n\t\t{/if}\n\t</div>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/map/colors.ts",
    "content": "import { colord } from 'colord';\n\nexport function getDecorativeColors(baseColor: string) {\n\tconst outlineColor = colord(baseColor).darken(0.2).toHex();\n\tconst tinted = colord(baseColor).isDark()\n\t\t? colord(baseColor).lighten(0.35)\n\t\t: colord(baseColor).darken(0.35);\n\tconst chevronColor = tinted.alpha(0.85).toRgbString();\n\n\treturn { outlineColor, chevronColor };\n}\n"
  },
  {
    "path": "ui/src/lib/map/createTripIcon.ts",
    "content": "import { browser } from '$app/environment';\n\nexport function createTripIcon(size: number): HTMLCanvasElement | undefined {\n\tif (!browser) {\n\t\treturn undefined;\n\t}\n\n\tconst border = (2 / 64) * size;\n\tconst padding = (size - size / 2) / 2 + border;\n\tconst innerSize = size - 2 * padding;\n\tconst mid = size / 2;\n\tconst rad = innerSize / 3.5;\n\tconst cv = document.createElement('canvas');\n\tcv.width = size;\n\tcv.height = size;\n\tconst ctx = cv.getContext('2d', { alpha: true });\n\n\tif (!ctx) {\n\t\treturn cv;\n\t}\n\n\tctx.beginPath();\n\n\tctx.arc(padding + rad, mid, rad, (1 / 2) * Math.PI, (3 / 2) * Math.PI, false);\n\n\tctx.bezierCurveTo(padding + rad + rad, mid - rad, size - padding, mid, size - padding, mid);\n\tctx.bezierCurveTo(size - padding, mid, padding + rad + rad, mid + rad, padding + rad, mid + rad);\n\n\tctx.closePath();\n\n\tctx.fillStyle = 'rgba(255, 0, 0, 0.7)';\n\tctx.fill();\n\tctx.lineWidth = border;\n\tctx.strokeStyle = 'rgba(120, 120, 120, 1.0)';\n\tctx.stroke();\n\treturn cv;\n}\n"
  },
  {
    "path": "ui/src/lib/map/getModeLabel.ts",
    "content": "import type { Mode } from '@motis-project/motis-client';\n\nexport const getModeLabel = (mode: Mode): string => {\n\tswitch (mode) {\n\t\tcase 'BUS':\n\t\tcase 'FERRY':\n\t\tcase 'TRAM':\n\t\tcase 'COACH':\n\t\tcase 'AIRPLANE':\n\t\tcase 'AERIAL_LIFT':\n\t\t\treturn 'Platform';\n\t\tdefault:\n\t\t\treturn 'Track';\n\t}\n};\n"
  },
  {
    "path": "ui/src/lib/map/handleScroll.ts",
    "content": "import { page } from '$app/state';\nimport { replaceState } from '$app/navigation';\n\nexport const restoreScroll = (container: HTMLElement) => {\n\tconst saveScroll = () => {\n\t\tpage.state.scrollY = container.scrollTop;\n\t\treplaceState('', page.state);\n\t};\n\n\tconst handlePopState = (event: PopStateEvent) => {\n\t\trequestAnimationFrame(() => {\n\t\t\tcontainer.scrollTop = event.state?.['sveltekit:states']?.scrollY ?? 0;\n\t\t});\n\t};\n\n\tcontainer.addEventListener('scrollend', saveScroll);\n\twindow.addEventListener('popstate', handlePopState);\n\n\treturn () => {\n\t\tcontainer.removeEventListener('scrollend', saveScroll);\n\t\twindow.removeEventListener('popstate', handlePopState);\n\t};\n};\n\nexport const resetScroll = (container: HTMLElement) => {\n\tif (page.state.scrollY == undefined) {\n\t\tcontainer.scrollTop = 0;\n\t}\n};\n"
  },
  {
    "path": "ui/src/lib/map/itineraries/ItineraryGeoJSON.svelte",
    "content": "<script lang=\"ts\">\n\timport Layer from '$lib/map/Layer.svelte';\n\timport GeoJSON from '$lib/map/GeoJSON.svelte';\n\timport type { Itinerary, Mode } from '@motis-project/motis-client';\n\timport { getColor } from '$lib/modeStyle';\n\timport polyline from '@mapbox/polyline';\n\timport { getDecorativeColors } from '$lib/map/colors';\n\timport { layers } from './itineraryLayers';\n\texport const PRECISION = 6;\n\n\tconst {\n\t\titinerary,\n\t\tid,\n\t\tselected,\n\t\tselectItinerary,\n\t\tlevel,\n\t\ttheme\n\t}: {\n\t\titinerary: Itinerary;\n\t\tid?: string;\n\t\tselected: boolean;\n\t\tselectItinerary?: () => void;\n\t\tlevel: number;\n\t\ttheme: 'light' | 'dark';\n\t} = $props();\n\n\tfunction isIndividualTransport(m: Mode): boolean {\n\t\treturn m == 'WALK' || m == 'BIKE' || m == 'CAR';\n\t}\n\n\tfunction getIndividualModeColor(m: Mode): string {\n\t\tswitch (m) {\n\t\t\tcase 'CAR':\n\t\t\t\treturn '#bf75ff';\n\t\t\tdefault:\n\t\t\t\treturn '#42a5f5';\n\t\t}\n\t}\n\n\tfunction itineraryToGeoJSON(i: Itinerary): GeoJSON.GeoJSON {\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: i.legs.flatMap((l) => {\n\t\t\t\tif (l.steps) {\n\t\t\t\t\tconst color = isIndividualTransport(l.mode)\n\t\t\t\t\t\t? getIndividualModeColor(l.mode)\n\t\t\t\t\t\t: `${getColor(l)[0]}`;\n\t\t\t\t\tconst { outlineColor, chevronColor } = getDecorativeColors(color);\n\t\t\t\t\treturn l.steps.map((p) => {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tcolor,\n\t\t\t\t\t\t\t\toutlineColor,\n\t\t\t\t\t\t\t\tchevronColor,\n\t\t\t\t\t\t\t\tfromLevel: p.fromLevel,\n\t\t\t\t\t\t\t\ttoLevel: p.toLevel,\n\t\t\t\t\t\t\t\tlevel: level,\n\t\t\t\t\t\t\t\tway: p.osmWay\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tgeometry: {\n\t\t\t\t\t\t\t\ttype: 'LineString',\n\t\t\t\t\t\t\t\tcoordinates: polyline.decode(p.polyline.points, PRECISION).map(([x, y]) => [y, x])\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconst color = `${getColor(l)[0]}`;\n\t\t\t\t\tconst { outlineColor, chevronColor } = getDecorativeColors(color);\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\toutlineColor,\n\t\t\t\t\t\t\tcolor,\n\t\t\t\t\t\t\tchevronColor\n\t\t\t\t\t\t},\n\t\t\t\t\t\tgeometry: {\n\t\t\t\t\t\t\ttype: 'LineString',\n\t\t\t\t\t\t\tcoordinates: polyline.decode(l.legGeometry.points, PRECISION).map(([x, y]) => [y, x])\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t})\n\t\t};\n\t}\n\tconst geojson = $derived(itineraryToGeoJSON(itinerary));\n</script>\n\n<GeoJSON id=\"route-{id}\" data={geojson}>\n\t{#each layers as layer (layer.id)}\n\t\t{#if !('line-gradient' in layer.paint) && selected}\n\t\t\t<Layer\n\t\t\t\tid={layer.id}\n\t\t\t\ttype={layer.type}\n\t\t\t\tlayout={layer.layout}\n\t\t\t\tfilter={['all', ['has', 'fromLevel'], layer.filter]}\n\t\t\t\tpaint={layer.paint}\n\t\t\t></Layer>\n\t\t{/if}\n\t{/each}\n\t<Layer\n\t\tid=\"path-{id}\"\n\t\ttype=\"line\"\n\t\tlayout={{\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t}}\n\t\tfilter={['!', ['has', 'fromLevel']]}\n\t\tonclick={selectItinerary\n\t\t\t? (_) => {\n\t\t\t\t\tselectItinerary();\n\t\t\t\t}\n\t\t\t: undefined}\n\t\tpaint={{\n\t\t\t'line-color': selected ? ['get', 'color'] : theme == 'dark' ? '#777' : '#bbb',\n\t\t\t'line-width': 7.5,\n\t\t\t'line-opacity': 1\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"path-outline-{id}\"\n\t\ttype=\"line\"\n\t\tlayout={{\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t}}\n\t\tfilter={['!', ['has', 'fromLevel']]}\n\t\tpaint={{\n\t\t\t'line-color': selected ? ['get', 'outlineColor'] : theme == 'dark' ? '#444' : '#999',\n\t\t\t'line-width': 1.5,\n\t\t\t'line-gap-width': 7.5,\n\t\t\t'line-opacity': 1\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"path-chevrons-{id}\"\n\t\ttype=\"symbol\"\n\t\tlayout={{\n\t\t\t'symbol-placement': 'line',\n\t\t\t'symbol-spacing': 40,\n\t\t\t'text-field': '›',\n\t\t\t'text-size': 24,\n\t\t\t'text-font': ['Noto Sans Bold'],\n\t\t\t'text-keep-upright': false,\n\t\t\t'text-allow-overlap': true,\n\t\t\t'text-rotation-alignment': 'map',\n\t\t\t'text-offset': [0, -0.1]\n\t\t}}\n\t\tfilter={['!', ['has', 'fromLevel']]}\n\t\tpaint={{\n\t\t\t'text-color': selected ? ['get', 'chevronColor'] : theme == 'dark' ? '#999' : '#ddd',\n\t\t\t'text-opacity': 0.85,\n\t\t\t'text-halo-color': selected ? ['get', 'outlineColor'] : theme == 'dark' ? '#444' : '#999',\n\t\t\t'text-halo-width': 0.5,\n\t\t\t'text-halo-blur': 0.2\n\t\t}}\n\t/>\n</GeoJSON>\n<GeoJSON id=\"route-{id}-metrics\" data={geojson} lineMetrics={true}>\n\t{#each layers as layer (layer.id)}\n\t\t{#if 'line-gradient' in layer.paint && selected}\n\t\t\t<Layer\n\t\t\t\tid=\"{layer.id}-metrics\"\n\t\t\t\ttype={layer.type}\n\t\t\t\tlayout={layer.layout}\n\t\t\t\tfilter={layer.filter}\n\t\t\t\tpaint={layer.paint}\n\t\t\t></Layer>\n\t\t{/if}\n\t{/each}\n</GeoJSON>\n"
  },
  {
    "path": "ui/src/lib/map/itineraries/itineraryLayers.ts",
    "content": "import {\n\tisLowerLevelRoutingFilter,\n\tisUpperLevelRoutingFilter,\n\tisCurrentLevelRoutingFilter,\n\tleadsToLowerLevelRoutingFilter,\n\tleadsUpToCurrentLevelRoutingFilter,\n\tleadsDownToCurrentLevelRoutingFilter,\n\tleadsToUpperLevelRoutingFilter\n} from './layerFilters';\n\n/// Routing path current level line color.\nconst routingPathFillColor = '#42a5f5';\n/// Routing path current level line outline color.\nconst routingPathOutlineColor = '#0077c2';\n/// Routing path other level line color.\nconst routingPathOtherLevelFillColor = '#aaaaaa';\n/// Routing path other level line outline color.\nconst routingPathOtherLevelOutlineColor = '#555555';\n/// Routing path line color.\nconst routingPathWidth = 7;\n/// Routing path line color.\nconst routingPathOutlineWidth = routingPathWidth + 2;\n\nexport const layers = [\n\t// Indoor routing - Outline - Current level \\\\\n\n\t{\n\t\tid: 'indoor-routing-path-current-outline',\n\t\ttype: 'line',\n\t\tfilter: isCurrentLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-color': routingPathOutlineColor,\n\t\t\t'line-width': routingPathOutlineWidth\n\t\t}\n\t},\n\n\t// Indoor routing - Lower level connecting path segments \\\\\n\n\t{\n\t\tid: 'indoor-routing-lower-path-down-outline',\n\t\ttype: 'line',\n\t\tfilter: leadsToLowerLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathOutlineWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathOutlineColor,\n\t\t\t\t1,\n\t\t\t\troutingPathOtherLevelOutlineColor\n\t\t\t]\n\t\t}\n\t},\n\t{\n\t\tid: 'indoor-routing-lower-path-down',\n\t\ttype: 'line',\n\t\tfilter: leadsToLowerLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathFillColor,\n\t\t\t\t1,\n\t\t\t\troutingPathOtherLevelFillColor\n\t\t\t]\n\t\t}\n\t},\n\t{\n\t\tid: 'indoor-routing-lower-path-up-outline',\n\t\ttype: 'line',\n\t\tfilter: leadsUpToCurrentLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathOutlineWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathOtherLevelOutlineColor,\n\t\t\t\t1,\n\t\t\t\troutingPathOutlineColor\n\t\t\t]\n\t\t}\n\t},\n\t{\n\t\tid: 'indoor-routing-lower-path-up',\n\t\ttype: 'line',\n\t\tfilter: leadsUpToCurrentLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathOtherLevelFillColor,\n\t\t\t\t1,\n\t\t\t\troutingPathFillColor\n\t\t\t]\n\t\t}\n\t},\n\n\t// Indoor routing - Outline - Upper level connecting path segments \\\\\n\n\t{\n\t\tid: 'indoor-routing-upper-path-down-outline',\n\t\ttype: 'line',\n\t\tfilter: leadsDownToCurrentLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathOutlineWidth,\n\t\t\t// 'line-gradient' must be specified using an expression\n\t\t\t// with the special 'line-progress' property\n\t\t\t// the source must have the 'lineMetrics' option set to true\n\t\t\t// note the line points have to be ordered so it fits (the direction of the line)\n\t\t\t// because no other expression are supported here\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathOtherLevelOutlineColor,\n\t\t\t\t1,\n\t\t\t\troutingPathOutlineColor\n\t\t\t]\n\t\t}\n\t},\n\t{\n\t\tid: 'indoor-routing-upper-path-up-outline',\n\t\ttype: 'line',\n\t\tfilter: leadsToUpperLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathOutlineWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathOutlineColor,\n\t\t\t\t1,\n\t\t\t\troutingPathOtherLevelOutlineColor\n\t\t\t]\n\t\t}\n\t},\n\n\t// Indoor routing - Concealed edges outline - Below current level \\\\\n\n\t{\n\t\tid: 'indoor-routing-path-concealed-below-outline',\n\t\t// required, otherwise line-dasharray will scale with metrics\n\t\ttype: 'line',\n\t\tfilter: isLowerLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-color': routingPathOtherLevelOutlineColor,\n\t\t\t'line-width': 2,\n\t\t\t'line-gap-width': 6,\n\t\t\t'line-dasharray': ['literal', [2, 2]]\n\t\t}\n\t},\n\n\t// Indoor routing - Outline - Above current level \\\\\n\n\t{\n\t\tid: 'indoor-routing-path-above-outline',\n\t\ttype: 'line',\n\t\tfilter: isUpperLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-color': routingPathOtherLevelOutlineColor,\n\t\t\t'line-width': routingPathOutlineWidth\n\t\t}\n\t},\n\n\t// Indoor routing - Fill - Current level \\\\\n\n\t{\n\t\tid: 'indoor-routing-path-current',\n\t\ttype: 'line',\n\t\tfilter: isCurrentLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-color': routingPathFillColor,\n\t\t\t'line-width': routingPathWidth\n\t\t}\n\t},\n\n\t// Indoor routing - Fill - Upper level connecting path segments \\\\\n\n\t{\n\t\tid: 'indoor-routing-upper-path-down',\n\t\ttype: 'line',\n\t\tfilter: leadsDownToCurrentLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathOtherLevelFillColor,\n\t\t\t\t1,\n\t\t\t\troutingPathFillColor\n\t\t\t]\n\t\t}\n\t},\n\t{\n\t\tid: 'indoor-routing-upper-path-up',\n\t\ttype: 'line',\n\t\tfilter: leadsToUpperLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-width': routingPathWidth,\n\t\t\t'line-gradient': [\n\t\t\t\t'interpolate',\n\t\t\t\t['linear'],\n\t\t\t\t['line-progress'],\n\t\t\t\t0,\n\t\t\t\troutingPathFillColor,\n\t\t\t\t1,\n\t\t\t\troutingPathOtherLevelFillColor\n\t\t\t]\n\t\t}\n\t},\n\n\t// Indoor routing - Fill - Above current level \\\\\n\n\t{\n\t\tid: 'indoor-routing-path-above',\n\t\ttype: 'line',\n\t\tfilter: isUpperLevelRoutingFilter,\n\t\tlayout: {\n\t\t\t'line-join': 'round',\n\t\t\t'line-cap': 'round'\n\t\t},\n\t\tpaint: {\n\t\t\t'line-color': routingPathOtherLevelFillColor,\n\t\t\t'line-width': routingPathWidth\n\t\t}\n\t}\n] as const;\n"
  },
  {
    "path": "ui/src/lib/map/itineraries/layerFilters.ts",
    "content": "// routing layer \\\\\n\nimport type { ExpressionFilterSpecification } from 'maplibre-gl';\n\nexport const currentLevel: ExpressionFilterSpecification = ['coalesce', ['get', 'level'], 0];\nexport const ceilFromLevel: ExpressionFilterSpecification = [\n\t'coalesce',\n\t['ceil', ['to-number', ['get', 'fromLevel']]],\n\t0\n];\nexport const ceilToLevel: ExpressionFilterSpecification = [\n\t'coalesce',\n\t['ceil', ['to-number', ['get', 'toLevel']]],\n\t0\n];\nexport const floorFromLevel: ExpressionFilterSpecification = [\n\t'coalesce',\n\t['floor', ['to-number', ['get', 'fromLevel']]],\n\t0\n];\nexport const floorToLevel: ExpressionFilterSpecification = [\n\t'coalesce',\n\t['floor', ['to-number', ['get', 'toLevel']]],\n\t0\n];\n\n/// Filter to match all connections that lie on, cross or connect to the current level.\n\nexport const connectsToCurrentLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'all',\n\t['<=', ['min', floorToLevel, floorFromLevel], currentLevel],\n\t['>=', ['max', ceilToLevel, ceilFromLevel], currentLevel]\n];\n\n/// Filter to match path connections on the current level.\n\nexport const isCurrentLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'any',\n\t['all', ['==', ceilFromLevel, currentLevel], ['==', ceilToLevel, currentLevel]],\n\t['all', ['==', floorFromLevel, currentLevel], ['==', floorToLevel, currentLevel]]\n];\n\n/// Filter to match path connections on any lower level that do not connect to current level.\n\nexport const isLowerLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'any',\n\t['all', ['<', ceilFromLevel, currentLevel], ['<', ceilToLevel, currentLevel]],\n\t['all', ['<', floorFromLevel, currentLevel], ['<', floorToLevel, currentLevel]]\n];\n\n/// Filter to match path connections on any upper level that do not connect to current level.\n\nexport const isUpperLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'any',\n\t['all', ['>', ceilFromLevel, currentLevel], ['>', ceilToLevel, currentLevel]],\n\t['all', ['>', floorFromLevel, currentLevel], ['>', floorToLevel, currentLevel]]\n];\n\n/// Filter to match paths that act as a connection from the current level to the upper level.\n\nexport const leadsToUpperLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'all',\n\t['any', ['==', ceilFromLevel, currentLevel], ['==', floorFromLevel, currentLevel]],\n\t['any', ['>', ceilToLevel, currentLevel], ['>', floorToLevel, currentLevel]]\n];\n\n/// Filter to match paths that act as a connection from the upper level to the current level.\n\nexport const leadsDownToCurrentLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'all',\n\t['any', ['>', ceilFromLevel, currentLevel], ['>', floorFromLevel, currentLevel]],\n\t['any', ['==', ceilToLevel, currentLevel], ['==', floorToLevel, currentLevel]]\n];\n\n/// Filter to match paths that act as a connection from the current level to the lower level.\n\nexport const leadsToLowerLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'all',\n\t['any', ['==', ceilFromLevel, currentLevel], ['==', floorFromLevel, currentLevel]],\n\t['any', ['<', ceilToLevel, currentLevel], ['<', floorToLevel, currentLevel]]\n];\n\n/// Filter to match paths that act as a connection from the lower level to the current level.\n\nexport const leadsUpToCurrentLevelRoutingFilter: ExpressionFilterSpecification = [\n\t'all',\n\t['any', ['<', ceilFromLevel, currentLevel], ['<', floorFromLevel, currentLevel]],\n\t['any', ['==', ceilToLevel, currentLevel], ['==', floorToLevel, currentLevel]]\n];\n\n// indoor tile layer \\\\\n\nexport const ceilLevel: ExpressionFilterSpecification = [\n\t'coalesce',\n\t['ceil', ['to-number', ['get', 'level']]],\n\t0\n];\nexport const floorLevel: ExpressionFilterSpecification = [\n\t'coalesce',\n\t['floor', ['to-number', ['get', 'level']]],\n\t0\n];\n\n/// Filter to only show element if level matches current level.\n///\n/// This will show features with level 0.5; 0.3; 0.7 on level 0 and on level 1\n\nexport const isCurrentLevelFilter: ExpressionFilterSpecification = [\n\t'any',\n\t['==', ceilLevel, currentLevel],\n\t['==', floorLevel, currentLevel]\n];\n\n/// Filter to match **any** level below the current level.\n\nexport const isLowerLevelFilter: ExpressionFilterSpecification = [\n\t// important that ceil and floor need to be lower\n\t'all',\n\t['<', ceilLevel, currentLevel],\n\t['<', floorLevel, currentLevel]\n];\n"
  },
  {
    "path": "ui/src/lib/map/rentals/Rentals.svelte",
    "content": "<script lang=\"ts\">\n\timport { browser } from '$app/environment';\n\timport {\n\t\trentals,\n\t\ttype RentalFormFactor,\n\t\ttype RentalProviderGroup,\n\t\ttype RentalProvider,\n\t\ttype RentalStation,\n\t\ttype RentalVehicle,\n\t\ttype RentalZone,\n\t\ttype MultiPolygon\n\t} from '@motis-project/motis-client';\n\timport { lngLatToStr } from '$lib/lngLatToStr';\n\timport Control from '$lib/map/Control.svelte';\n\timport GeoJSON from '$lib/map/GeoJSON.svelte';\n\timport Layer from '$lib/map/Layer.svelte';\n\timport {\n\t\tDEFAULT_FORM_FACTOR,\n\t\tICON_TYPES,\n\t\tformFactorAssets,\n\t\tcolorizeIcon,\n\t\tgetIconBaseName,\n\t\tgetIconUrl,\n\t\tgetIconDimensions,\n\t\ttype IconType\n\t} from '$lib/map/rentals/assets';\n\timport { cn } from '$lib/utils';\n\timport polyline from '@mapbox/polyline';\n\timport maplibregl from 'maplibre-gl';\n\timport type { GeoJSONSource, MapLayerMouseEvent } from 'maplibre-gl';\n\timport { mount, onDestroy, unmount } from 'svelte';\n\timport type { FeatureCollection, Point, Position } from 'geojson';\n\timport RBush from 'rbush';\n\timport { point } from '@turf/helpers';\n\timport { booleanPointInPolygon } from '@turf/boolean-point-in-polygon';\n\timport StationPopup from '$lib/map/rentals/StationPopup.svelte';\n\timport VehiclePopup from '$lib/map/rentals/VehiclePopup.svelte';\n\timport ZonePopup from '$lib/map/rentals/ZonePopup.svelte';\n\timport {\n\t\tDEFAULT_COLOR,\n\t\tzoomScaledIconSize,\n\t\tzoomScaledTextOffset,\n\t\tzoomScaledTextSizeMedium,\n\t\tzoomScaledTextSizeSmall\n\t} from './style';\n\timport ZoneLayer from './ZoneLayer.svelte';\n\timport type {\n\t\tRentalZoneFeature,\n\t\tRentalZoneFeatureCollection,\n\t\tRentalZoneFeatureProperties\n\t} from './zone-types';\n\timport { SvelteSet } from 'svelte/reactivity';\n\n\tlet zoneLayerRef = $state<ZoneLayer | null>(null);\n\n\tlet {\n\t\tmap,\n\t\tbounds,\n\t\tzoom,\n\t\ttheme,\n\t\tdebug = false\n\t}: {\n\t\tmap: maplibregl.Map | undefined;\n\t\tbounds: maplibregl.LngLatBoundsLike | undefined;\n\t\tzoom: number;\n\t\ttheme: 'light' | 'dark';\n\t\tdebug?: boolean;\n\t} = $props();\n\n\tconst MIN_ZOOM = 11;\n\tconst FETCH_PADDING_RATIO = 0.5;\n\tconst STATION_SOURCE_ID = 'rentals-stations';\n\tconst STATION_ICON_LAYER_ID = 'rentals-stations-icons';\n\tconst VEHICLE_SOURCE_PREFIX = 'rentals-vehicles';\n\tconst VEHICLE_CLUSTER_RADIUS = 50;\n\tconst ZONE_LAYER_ID = 'rentals-zones';\n\n\tconst formFactors = Object.keys(formFactorAssets) as RentalFormFactor[];\n\n\tconst getIconId = (formFactor: RentalFormFactor, type: IconType, color: string) =>\n\t\t`${getIconBaseName(formFactor, type)}-${color}`;\n\n\ttype RentalsPayload = Awaited<ReturnType<typeof rentals>>['data'];\n\ttype RentalsPayloadData = NonNullable<RentalsPayload>;\n\n\ttype DisplayFilter = {\n\t\tproviderGroupId: string;\n\t\tproviderGroupName: string;\n\t\tformFactor: RentalFormFactor;\n\t\tcolor: string;\n\t};\n\n\tlet rentalsData = $state<RentalsPayload | null>(null);\n\tlet loadedBounds = $state<maplibregl.LngLatBounds | null>(null);\n\tlet requestToken = 0;\n\tlet displayFilter = $state<DisplayFilter | null>(null);\n\tlet iconsReady = $state(false);\n\tlet iconRequestToken = 0;\n\tlet activeIconIds = new SvelteSet<string>();\n\n\ttype StationFeatureProperties = {\n\t\ticon: string;\n\t\tavailable: number;\n\t\tproviderId: string;\n\t\tstationId: string;\n\t\tcolor: string;\n\t};\n\n\ttype VehicleFeatureProperties = {\n\t\ticon: string;\n\t\tclusterIcon: string;\n\t\tproviderId: string;\n\t\tvehicleId: string;\n\t\tcolor: string;\n\t};\n\n\ttype VehicleCollections = Record<\n\t\tRentalFormFactor,\n\t\tFeatureCollection<Point, VehicleFeatureProperties>\n\t>;\n\n\tconst vehicleLayerConfigs = formFactors.map((formFactor) => {\n\t\tconst slug = formFactor.toLowerCase();\n\t\treturn {\n\t\t\tformFactor,\n\t\t\tsourceId: `${VEHICLE_SOURCE_PREFIX}-${slug}`,\n\t\t\tclusterLayerId: `${VEHICLE_SOURCE_PREFIX}-${slug}-cluster`,\n\t\t\tpointLayerId: `${VEHICLE_SOURCE_PREFIX}-${slug}-point`\n\t\t};\n\t});\n\n\tconst buildProviderGroupOptions = (providerGroups: RentalProviderGroup[]): DisplayFilter[] =>\n\t\tproviderGroups\n\t\t\t.flatMap((group) =>\n\t\t\t\tgroup.formFactors.map((formFactor) => ({\n\t\t\t\t\tproviderGroupId: group.id,\n\t\t\t\t\tproviderGroupName: group.name,\n\t\t\t\t\tformFactor,\n\t\t\t\t\tcolor: group.color || DEFAULT_COLOR\n\t\t\t\t}))\n\t\t\t)\n\t\t\t.sort(\n\t\t\t\t(a, b) =>\n\t\t\t\t\ta.providerGroupName.localeCompare(b.providerGroupName, undefined, {\n\t\t\t\t\t\tsensitivity: 'base'\n\t\t\t\t\t}) || a.formFactor.localeCompare(b.formFactor, undefined, { sensitivity: 'base' })\n\t\t\t);\n\n\tlet providerGroupOptions = $derived(\n\t\trentalsData ? buildProviderGroupOptions(rentalsData.providerGroups) : []\n\t);\n\n\tlet providerGroupColors = $derived.by(\n\t\t() =>\n\t\t\tnew Map(\n\t\t\t\t(rentalsData?.providerGroups ?? []).map((group) => [group.id, group.color || DEFAULT_COLOR])\n\t\t\t)\n\t);\n\n\tlet providerColors = $derived.by(\n\t\t() =>\n\t\t\tnew Map(\n\t\t\t\t(rentalsData?.providers ?? []).map((provider) => [\n\t\t\t\t\tprovider.id,\n\t\t\t\t\tprovider.color || providerGroupColors.get(provider.groupId) || DEFAULT_COLOR\n\t\t\t\t])\n\t\t\t)\n\t);\n\n\ttype VehicleTypeFormFactors = Record<string, RentalFormFactor>;\n\n\tlet vehicleTypeFormFactorsByProviderId = $derived.by(\n\t\t(): Record<string, VehicleTypeFormFactors> => {\n\t\t\tif (!rentalsData) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\treturn Object.fromEntries(\n\t\t\t\trentalsData.providers.map((provider) => [\n\t\t\t\t\tprovider.id,\n\t\t\t\t\tObject.fromEntries(provider.vehicleTypes.map((type) => [type.id, type.formFactor]))\n\t\t\t\t])\n\t\t\t);\n\t\t}\n\t);\n\n\tconst isSameFilter = (a: DisplayFilter | null, b: DisplayFilter | null) =>\n\t\ta?.providerGroupId === b?.providerGroupId && a?.formFactor === b?.formFactor;\n\n\tconst toggleFilter = (option: DisplayFilter) => {\n\t\tdisplayFilter = isSameFilter(displayFilter, option) ? null : option;\n\t};\n\n\tconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\n\tconst expandBounds = (value: maplibregl.LngLatBounds) => {\n\t\tconst sw = value.getSouthWest();\n\t\tconst ne = value.getNorthEast();\n\t\tconst padLng = (ne.lng - sw.lng) * FETCH_PADDING_RATIO;\n\t\tconst padLat = (ne.lat - sw.lat) * FETCH_PADDING_RATIO;\n\t\treturn new maplibregl.LngLatBounds(\n\t\t\t[clamp(sw.lng - padLng, -180, 180), clamp(sw.lat - padLat, -90, 90)],\n\t\t\t[clamp(ne.lng + padLng, -180, 180), clamp(ne.lat + padLat, -90, 90)]\n\t\t);\n\t};\n\n\tconst boundsContain = (outer: maplibregl.LngLatBounds | null, inner: maplibregl.LngLatBounds) =>\n\t\t!!outer && outer.contains(inner.getSouthWest()) && outer.contains(inner.getNorthEast());\n\n\tconst fetchRentals = async (mapBounds: maplibregl.LngLatBounds) => {\n\t\tconst expandedBounds = expandBounds(mapBounds);\n\t\tconst max = lngLatToStr(expandedBounds.getNorthWest());\n\t\tconst min = lngLatToStr(expandedBounds.getSouthEast());\n\t\tconst token = ++requestToken;\n\t\tconsole.debug('[Rentals] requesting rentals', { min, max });\n\n\t\tconst { data, error } = await rentals({ query: { max, min } });\n\t\tif (token !== requestToken) {\n\t\t\treturn;\n\t\t}\n\t\tif (error) {\n\t\t\tconsole.error('[Rentals] rentals request failed', error);\n\t\t\treturn;\n\t\t}\n\n\t\trentalsData = data;\n\t\tloadedBounds = expandedBounds;\n\t\tconst current = data;\n\t\tconsole.debug('[Rentals] received rentals', {\n\t\t\tproviders: current.providers.length,\n\t\t\tstations: current.stations.length,\n\t\t\tvehicles: current.vehicles.length,\n\t\t\tzones: current.zones.length\n\t\t});\n\t};\n\n\tconst collectFormFactorsByColor = (data: RentalsPayloadData) => {\n\t\t/* eslint-disable svelte/prefer-svelte-reactivity */\n\t\tconst formFactorsByColor = new Map<string, Set<RentalFormFactor>>();\n\t\tconst providerColorById = new Map<string, string>();\n\t\tconst providerGroupColorById = new Map<string, string>(\n\t\t\tdata.providerGroups.map((group) => [group.id, group.color || DEFAULT_COLOR])\n\t\t);\n\t\t/* eslint-enable svelte/prefer-svelte-reactivity */\n\n\t\tconst addFormFactor = (color: string, formFactor: RentalFormFactor) => {\n\t\t\tlet formFactors = formFactorsByColor.get(color);\n\t\t\tif (!formFactors) {\n\t\t\t\t/* eslint-disable-next-line svelte/prefer-svelte-reactivity */\n\t\t\t\tformFactors = new Set<RentalFormFactor>();\n\t\t\t\tformFactorsByColor.set(color, formFactors);\n\t\t\t}\n\t\t\tformFactors.add(formFactor);\n\t\t};\n\n\t\tfor (const provider of data.providers) {\n\t\t\tconst color = provider.color || providerGroupColorById.get(provider.groupId) || DEFAULT_COLOR;\n\t\t\tproviderColorById.set(provider.id, color);\n\t\t\tfor (const formFactor of provider.formFactors) {\n\t\t\t\taddFormFactor(color, formFactor);\n\t\t\t}\n\t\t}\n\t\tfor (const station of data.stations) {\n\t\t\tconst color =\n\t\t\t\tproviderColorById.get(station.providerId) ||\n\t\t\t\tproviderGroupColorById.get(station.providerGroupId) ||\n\t\t\t\tDEFAULT_COLOR;\n\t\t\tif (station.formFactors.length === 0) {\n\t\t\t\taddFormFactor(color, DEFAULT_FORM_FACTOR);\n\t\t\t}\n\t\t\tfor (const formFactor of station.formFactors) {\n\t\t\t\taddFormFactor(color, formFactor);\n\t\t\t}\n\t\t}\n\t\tfor (const vehicle of data.vehicles) {\n\t\t\tconst color =\n\t\t\t\tproviderColorById.get(vehicle.providerId) ||\n\t\t\t\tproviderGroupColorById.get(vehicle.providerGroupId) ||\n\t\t\t\tDEFAULT_COLOR;\n\t\t\taddFormFactor(color, vehicle.formFactor);\n\t\t}\n\t\treturn formFactorsByColor;\n\t};\n\n\tconst stationAvailability = (\n\t\tstation: RentalStation,\n\t\tformFactorsByProvider: Record<string, VehicleTypeFormFactors>,\n\t\tfilter: DisplayFilter | null\n\t) => {\n\t\tif (!filter) {\n\t\t\treturn station.numVehiclesAvailable;\n\t\t}\n\t\tconst formFactors = formFactorsByProvider[station.providerId];\n\t\tif (!formFactors) {\n\t\t\treturn station.numVehiclesAvailable;\n\t\t}\n\t\treturn Object.entries(station.vehicleTypesAvailable).reduce((sum, [typeId, count]) => {\n\t\t\treturn formFactors[typeId] === filter.formFactor ? sum + count : sum;\n\t\t}, 0);\n\t};\n\n\tlet stationFeatures = $derived.by((): FeatureCollection<Point, StationFeatureProperties> => {\n\t\tif (!rentalsData || !iconsReady) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\t\tconst filter = displayFilter;\n\t\tconst stations = filter\n\t\t\t? rentalsData.stations.filter(\n\t\t\t\t\t(station) =>\n\t\t\t\t\t\tstation.providerGroupId === filter.providerGroupId &&\n\t\t\t\t\t\tstation.formFactors.includes(filter.formFactor)\n\t\t\t\t)\n\t\t\t: ([] as RentalStation[]);\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: stations.map((station) => {\n\t\t\t\tconst color =\n\t\t\t\t\tproviderColors.get(station.providerId) ||\n\t\t\t\t\tproviderGroupColors.get(station.providerGroupId) ||\n\t\t\t\t\tDEFAULT_COLOR;\n\t\t\t\tconst formFactor = filter\n\t\t\t\t\t? filter.formFactor\n\t\t\t\t\t: (station.formFactors[0] ?? DEFAULT_FORM_FACTOR);\n\t\t\t\treturn {\n\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\tgeometry: {\n\t\t\t\t\t\ttype: 'Point',\n\t\t\t\t\t\tcoordinates: [station.lon, station.lat]\n\t\t\t\t\t},\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\ticon: getIconId(formFactor, 'station', color),\n\t\t\t\t\t\tavailable: stationAvailability(station, vehicleTypeFormFactorsByProviderId, filter),\n\t\t\t\t\t\tproviderId: station.providerId,\n\t\t\t\t\t\tstationId: station.id,\n\t\t\t\t\t\tcolor\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t})\n\t\t};\n\t});\n\n\tlet vehicleCollections = $derived.by((): VehicleCollections => {\n\t\tconst collections = formFactors.reduce((acc, formFactor) => {\n\t\t\tacc[formFactor] = { type: 'FeatureCollection', features: [] };\n\t\t\treturn acc;\n\t\t}, {} as VehicleCollections);\n\t\tif (!rentalsData || !iconsReady) {\n\t\t\treturn collections;\n\t\t}\n\t\tconst filter = displayFilter;\n\t\tconst vehicles = filter\n\t\t\t? rentalsData.vehicles.filter(\n\t\t\t\t\t(vehicle) =>\n\t\t\t\t\t\tvehicle.providerGroupId === filter.providerGroupId &&\n\t\t\t\t\t\tvehicle.formFactor === filter.formFactor\n\t\t\t\t)\n\t\t\t: ([] as RentalVehicle[]);\n\t\tvehicles\n\t\t\t.filter((vehicle) => !vehicle.stationId)\n\t\t\t.forEach((vehicle) => {\n\t\t\t\tconst collection = collections[vehicle.formFactor];\n\t\t\t\tconst color =\n\t\t\t\t\tproviderColors.get(vehicle.providerId) ||\n\t\t\t\t\tproviderGroupColors.get(vehicle.providerGroupId) ||\n\t\t\t\t\tDEFAULT_COLOR;\n\t\t\t\tcollection.features.push({\n\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\tgeometry: {\n\t\t\t\t\t\ttype: 'Point',\n\t\t\t\t\t\tcoordinates: [vehicle.lon, vehicle.lat]\n\t\t\t\t\t},\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\ticon: getIconId(vehicle.formFactor, 'vehicle', color),\n\t\t\t\t\t\tclusterIcon: getIconId(vehicle.formFactor, 'cluster', color),\n\t\t\t\t\t\tproviderId: vehicle.providerId,\n\t\t\t\t\t\tvehicleId: vehicle.id,\n\t\t\t\t\t\tcolor\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\treturn collections;\n\t});\n\n\tconst buildZoneGeometry = (mp: MultiPolygon): RentalZoneFeature['geometry'] => {\n\t\treturn {\n\t\t\ttype: 'MultiPolygon',\n\t\t\tcoordinates: mp.map((polygon) =>\n\t\t\t\tpolygon.map((encoded) =>\n\t\t\t\t\tpolyline\n\t\t\t\t\t\t.decode(encoded.points, encoded.precision)\n\t\t\t\t\t\t.map(([lat, lng]) => [lng, lat] as Position)\n\t\t\t\t)\n\t\t\t)\n\t\t};\n\t};\n\n\tconst findMatchingRule = (\n\t\tzone: RentalZone,\n\t\tprovider: RentalProvider,\n\t\tformFactor: RentalFormFactor\n\t) => {\n\t\treturn zone.rules.find((rule) => {\n\t\t\tif (!rule.vehicleTypeIdxs.length) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn rule.vehicleTypeIdxs.some(\n\t\t\t\t(idx) => provider.vehicleTypes[idx]?.formFactor === formFactor\n\t\t\t);\n\t\t});\n\t};\n\n\tlet zoneFeatures = $derived.by((): RentalZoneFeatureCollection => {\n\t\tconst data = rentalsData;\n\t\tconst filter = displayFilter;\n\n\t\tif (!data || !filter) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\n\t\tconst features: RentalZoneFeature[] = [];\n\t\tlet maxGeofencingZ = 0;\n\n\t\tdata.zones.forEach((zone, index) => {\n\t\t\tif (zone.providerGroupId !== filter.providerGroupId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst provider = data.providers.find((candidate) => candidate.id === zone.providerId);\n\t\t\tif (!provider) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst rule = findMatchingRule(zone, provider, filter.formFactor);\n\t\t\tif (!rule) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfeatures.push({\n\t\t\t\ttype: 'Feature',\n\t\t\t\tgeometry: buildZoneGeometry(zone.area),\n\t\t\t\tproperties: {\n\t\t\t\t\tzoneIndex: index,\n\t\t\t\t\tproviderId: zone.providerId,\n\t\t\t\t\tz: zone.z,\n\t\t\t\t\trideEndAllowed: rule.rideEndAllowed && !rule.stationParking,\n\t\t\t\t\trideThroughAllowed: rule.rideThroughAllowed,\n\t\t\t\t\tstationArea: false\n\t\t\t\t}\n\t\t\t} satisfies RentalZoneFeature);\n\t\t\tif (zone.z > maxGeofencingZ) {\n\t\t\t\tmaxGeofencingZ = zone.z;\n\t\t\t}\n\t\t});\n\n\t\tconst stationAreaZ = maxGeofencingZ + 1;\n\t\tdata.stations.forEach((station, index) => {\n\t\t\tif (\n\t\t\t\t!station.stationArea ||\n\t\t\t\tstation.providerGroupId !== filter.providerGroupId ||\n\t\t\t\t!station.formFactors.includes(filter.formFactor)\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfeatures.push({\n\t\t\t\ttype: 'Feature',\n\t\t\t\tgeometry: buildZoneGeometry(station.stationArea),\n\t\t\t\tproperties: {\n\t\t\t\t\tstationIndex: index,\n\t\t\t\t\tproviderId: station.providerId,\n\t\t\t\t\tz: stationAreaZ,\n\t\t\t\t\trideEndAllowed: true,\n\t\t\t\t\trideThroughAllowed: true,\n\t\t\t\t\tstationArea: true\n\t\t\t\t}\n\t\t\t} satisfies RentalZoneFeature);\n\t\t});\n\n\t\tfeatures.sort((a, b) => b.properties.z - a.properties.z);\n\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures\n\t\t};\n\t});\n\n\tlet zoneRTree = $derived.by(() => {\n\t\tconst data = rentalsData;\n\t\tconst rtree = new RBush<{\n\t\t\tminX: number;\n\t\t\tminY: number;\n\t\t\tmaxX: number;\n\t\t\tmaxY: number;\n\t\t\tfeature: RentalZoneFeature;\n\t\t}>();\n\t\tif (data && debug) {\n\t\t\trtree.load(\n\t\t\t\tzoneFeatures.features.map((feature) => {\n\t\t\t\t\tconst bbox =\n\t\t\t\t\t\tfeature.properties.zoneIndex !== undefined\n\t\t\t\t\t\t\t? data.zones[feature.properties.zoneIndex].bbox\n\t\t\t\t\t\t\t: data.stations[feature.properties.stationIndex!].bbox;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tminX: bbox[0],\n\t\t\t\t\t\tminY: bbox[1],\n\t\t\t\t\t\tmaxX: bbox[2],\n\t\t\t\t\t\tmaxY: bbox[3],\n\t\t\t\t\t\tfeature\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t\treturn rtree;\n\t});\n\n\tconst createScopedId = (providerId: string, entityId: string) => `${providerId}::${entityId}`;\n\n\tlet providerById = $derived.by(\n\t\t(): Map<string, RentalProvider> => new Map(rentalsData?.providers.map((p) => [p.id, p]) ?? [])\n\t);\n\n\tlet stationById = $derived.by(\n\t\t(): Map<string, RentalStation> =>\n\t\t\tnew Map(rentalsData?.stations.map((s) => [createScopedId(s.providerId, s.id), s]) ?? [])\n\t);\n\n\tlet vehicleById = $derived.by(\n\t\t(): Map<string, RentalVehicle> =>\n\t\t\tnew Map(rentalsData?.vehicles.map((v) => [createScopedId(v.providerId, v.id), v]) ?? [])\n\t);\n\n\tconst lookupStation = (properties: StationFeatureProperties) => {\n\t\tconst key = createScopedId(properties.providerId, properties.stationId);\n\t\treturn {\n\t\t\tkey,\n\t\t\tprovider: providerById.get(properties.providerId),\n\t\t\tstation: stationById.get(key)\n\t\t};\n\t};\n\n\tconst lookupVehicle = (properties: VehicleFeatureProperties) => {\n\t\tconst key = createScopedId(properties.providerId, properties.vehicleId);\n\t\treturn {\n\t\t\tkey,\n\t\t\tprovider: providerById.get(properties.providerId),\n\t\t\tvehicle: vehicleById.get(key)\n\t\t};\n\t};\n\n\tconst lookupZoneOrStation = (properties: RentalZoneFeatureProperties) => {\n\t\tconst zone =\n\t\t\tproperties.zoneIndex !== undefined ? rentalsData?.zones[properties.zoneIndex] : undefined;\n\t\tconst station =\n\t\t\tproperties.stationIndex !== undefined\n\t\t\t\t? rentalsData?.stations[properties.stationIndex]\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\tkey:\n\t\t\t\tproperties.zoneIndex !== undefined\n\t\t\t\t\t? `zone-${properties.zoneIndex}`\n\t\t\t\t\t: `station-${properties.stationIndex}`,\n\t\t\tprovider: providerById.get(properties.providerId),\n\t\t\tzone,\n\t\t\tstation,\n\t\t\trideThroughAllowed: properties.rideThroughAllowed,\n\t\t\trideEndAllowed: properties.rideEndAllowed,\n\t\t\tstationArea: properties.stationArea\n\t\t};\n\t};\n\n\tconst createStationContent = (\n\t\tprovider: RentalProvider,\n\t\tstation: RentalStation,\n\t\tshowActions: boolean\n\t) => {\n\t\tconst container = document.createElement('div');\n\t\tconst component = mount(StationPopup, {\n\t\t\ttarget: container,\n\t\t\tprops: { provider, station, showActions, debug }\n\t\t});\n\t\treturn {\n\t\t\telement: container,\n\t\t\tdestroy: () => {\n\t\t\t\tunmount(component);\n\t\t\t}\n\t\t};\n\t};\n\n\tconst createVehicleContent = (\n\t\tprovider: RentalProvider,\n\t\tvehicle: RentalVehicle,\n\t\tshowActions: boolean\n\t) => {\n\t\tconst container = document.createElement('div');\n\t\tconst component = mount(VehiclePopup, {\n\t\t\ttarget: container,\n\t\t\tprops: { provider, vehicle, showActions, debug }\n\t\t});\n\t\treturn {\n\t\t\telement: container,\n\t\t\tdestroy: () => {\n\t\t\t\tunmount(component);\n\t\t\t}\n\t\t};\n\t};\n\n\tconst createZoneContent = (\n\t\tprovider: RentalProvider,\n\t\tzone: RentalZone | undefined,\n\t\tstation: RentalStation | undefined,\n\t\trideThroughAllowed: boolean,\n\t\trideEndAllowed: boolean,\n\t\tallZonesAtPoint: RentalZoneFeature[]\n\t) => {\n\t\tconst container = document.createElement('div');\n\t\tconst component = mount(ZonePopup, {\n\t\t\ttarget: container,\n\t\t\tprops: {\n\t\t\t\tprovider,\n\t\t\t\tzone,\n\t\t\t\tstation,\n\t\t\t\trideThroughAllowed,\n\t\t\t\trideEndAllowed,\n\t\t\t\tdebug,\n\t\t\t\tallZonesAtPoint,\n\t\t\t\tzoneData: rentalsData?.zones ?? [],\n\t\t\t\tstationData: rentalsData?.stations ?? []\n\t\t\t}\n\t\t});\n\t\treturn {\n\t\t\telement: container,\n\t\t\tdestroy: () => {\n\t\t\t\tunmount(component);\n\t\t\t}\n\t\t};\n\t};\n\n\tlet tooltipPopup: maplibregl.Popup | null = null;\n\tlet detailPopup: maplibregl.Popup | null = null;\n\tlet activeTooltipKey: string | null = null;\n\tlet activePopupKey: string | null = null;\n\tlet lastHoverCanvas: HTMLCanvasElement | null = null;\n\tlet tooltipContentDestroy: (() => void) | null = null;\n\tlet popupContentDestroy: (() => void) | null = null;\n\n\tconst ensureTooltipPopup = () => {\n\t\tif (!tooltipPopup) {\n\t\t\ttooltipPopup = new maplibregl.Popup({\n\t\t\t\tcloseButton: false,\n\t\t\t\tcloseOnClick: false,\n\t\t\t\toffset: 12,\n\t\t\t\tmaxWidth: 'none'\n\t\t\t});\n\t\t\ttooltipPopup.on('close', () => {\n\t\t\t\ttooltipContentDestroy?.();\n\t\t\t\ttooltipContentDestroy = null;\n\t\t\t\tactiveTooltipKey = null;\n\t\t\t});\n\t\t}\n\t\treturn tooltipPopup;\n\t};\n\n\tconst ensureDetailPopup = () => {\n\t\tif (!detailPopup) {\n\t\t\tdetailPopup = new maplibregl.Popup({\n\t\t\t\tcloseButton: true,\n\t\t\t\tcloseOnClick: true,\n\t\t\t\toffset: 12,\n\t\t\t\tmaxWidth: 'none'\n\t\t\t});\n\t\t\tdetailPopup.on('close', () => {\n\t\t\t\tpopupContentDestroy?.();\n\t\t\t\tpopupContentDestroy = null;\n\t\t\t\tactivePopupKey = null;\n\t\t\t});\n\t\t}\n\t\treturn detailPopup;\n\t};\n\n\tconst hideTooltip = () => {\n\t\ttooltipContentDestroy?.();\n\t\ttooltipContentDestroy = null;\n\t\tif (tooltipPopup) {\n\t\t\ttooltipPopup.remove();\n\t\t}\n\t\tactiveTooltipKey = null;\n\t};\n\n\tconst hidePopup = () => {\n\t\tpopupContentDestroy?.();\n\t\tpopupContentDestroy = null;\n\t\tif (detailPopup) {\n\t\t\tdetailPopup.remove();\n\t\t}\n\t\tactivePopupKey = null;\n\t};\n\n\tconst resetHoverCursor = () => {\n\t\tif (lastHoverCanvas) {\n\t\t\tlastHoverCanvas.style.cursor = '';\n\t\t\tlastHoverCanvas = null;\n\t\t}\n\t};\n\n\tconst showTooltip = (\n\t\ttargetMap: maplibregl.Map,\n\t\tlngLat: maplibregl.LngLatLike,\n\t\tkey: string,\n\t\tcreateContent: () => { element: HTMLElement; destroy: () => void }\n\t) => {\n\t\tconst popup = ensureTooltipPopup();\n\t\tif (activeTooltipKey !== key) {\n\t\t\ttooltipContentDestroy?.();\n\t\t\tconst { element, destroy } = createContent();\n\t\t\ttooltipContentDestroy = destroy;\n\t\t\tpopup.setDOMContent(element);\n\t\t\tactiveTooltipKey = key;\n\t\t}\n\t\tpopup.setLngLat(lngLat);\n\t\tif (!popup.isOpen()) {\n\t\t\tpopup.addTo(targetMap);\n\t\t}\n\t};\n\n\tconst showPopup = (\n\t\ttargetMap: maplibregl.Map,\n\t\tlngLat: maplibregl.LngLatLike,\n\t\tkey: string,\n\t\tcreateContent: () => { element: HTMLElement; destroy: () => void }\n\t) => {\n\t\tconst popup = ensureDetailPopup();\n\t\tpopupContentDestroy?.();\n\t\tconst { element, destroy } = createContent();\n\t\tpopupContentDestroy = destroy;\n\t\tpopup.setDOMContent(element);\n\t\tpopup.setLngLat(lngLat);\n\t\tif (!popup.isOpen()) {\n\t\t\tpopup.addTo(targetMap);\n\t\t}\n\t\tactivePopupKey = key;\n\t};\n\n\tconst createMouseMoveHandler =\n\t\t<T,>(\n\t\t\tlookup: (properties: T) => {\n\t\t\t\tkey: string;\n\t\t\t\tprovider: RentalProvider | undefined;\n\t\t\t\tstation?: RentalStation;\n\t\t\t\tvehicle?: RentalVehicle;\n\t\t\t},\n\t\t\tcreateContent: (\n\t\t\t\tprovider: RentalProvider,\n\t\t\t\tentity: RentalStation | RentalVehicle\n\t\t\t) => { element: HTMLElement; destroy: () => void }\n\t\t) =>\n\t\t(event: MapLayerMouseEvent, mapInstance: maplibregl.Map) => {\n\t\t\tconst feature = event.features?.[0];\n\t\t\tif (!mapInstance || !feature) {\n\t\t\t\thideTooltip();\n\t\t\t\tresetHoverCursor();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst result = lookup(feature.properties as T);\n\t\t\tconst entity = result.station || result.vehicle;\n\t\t\tif (!entity || !result.provider) {\n\t\t\t\thideTooltip();\n\t\t\t\tresetHoverCursor();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlastHoverCanvas = mapInstance.getCanvas();\n\t\t\tlastHoverCanvas.style.cursor = 'pointer';\n\t\t\tif (activePopupKey === result.key) {\n\t\t\t\thideTooltip();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowTooltip(mapInstance, event.lngLat, result.key, () =>\n\t\t\t\tcreateContent(result.provider!, entity)\n\t\t\t);\n\t\t};\n\n\tconst createMouseLeaveHandler =\n\t\t() => (_event: MapLayerMouseEvent, _mapInstance: maplibregl.Map) => {\n\t\t\tresetHoverCursor();\n\t\t\thideTooltip();\n\t\t};\n\n\tconst createClickHandler =\n\t\t<T,>(\n\t\t\tlookup: (properties: T) => {\n\t\t\t\tkey: string;\n\t\t\t\tprovider: RentalProvider | undefined;\n\t\t\t\tstation?: RentalStation;\n\t\t\t\tvehicle?: RentalVehicle;\n\t\t\t},\n\t\t\tcreateContent: (\n\t\t\t\tprovider: RentalProvider,\n\t\t\t\tentity: RentalStation | RentalVehicle\n\t\t\t) => { element: HTMLElement; destroy: () => void }\n\t\t) =>\n\t\t(event: MapLayerMouseEvent, mapInstance: maplibregl.Map) => {\n\t\t\tconst feature = event.features?.[0];\n\t\t\tif (!mapInstance || !feature) {\n\t\t\t\thidePopup();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst result = lookup(feature.properties as T);\n\t\t\tconst entity = result.station || result.vehicle;\n\t\t\tif (!entity || !result.provider) {\n\t\t\t\thidePopup();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\thideTooltip();\n\t\t\thidePopup();\n\t\t\tshowPopup(mapInstance, event.lngLat, result.key, () =>\n\t\t\t\tcreateContent(result.provider!, entity)\n\t\t\t);\n\t\t};\n\n\tconst handleStationMouseMove = createMouseMoveHandler(lookupStation, (provider, entity) =>\n\t\tcreateStationContent(provider, entity as RentalStation, true)\n\t);\n\tconst handleStationMouseLeave = createMouseLeaveHandler();\n\tconst handleStationClick = createClickHandler(lookupStation, (provider, entity) =>\n\t\tcreateStationContent(provider, entity as RentalStation, true)\n\t);\n\n\tconst handleClusterClick = async (event: MapLayerMouseEvent, mapInstance: maplibregl.Map) => {\n\t\tconst feature = event.features?.[0];\n\t\tconst clusterId = feature?.properties?.cluster_id;\n\t\tconst sourceId = feature?.source;\n\t\tif (!mapInstance || !feature || typeof clusterId !== 'number' || !sourceId) {\n\t\t\treturn;\n\t\t}\n\t\tconst source = mapInstance.getSource(sourceId) as GeoJSONSource | undefined;\n\t\tif (!source) {\n\t\t\treturn;\n\t\t}\n\n\t\thideTooltip();\n\t\thidePopup();\n\t\tmapInstance.getCanvas().style.cursor = '';\n\n\t\tconst zoom = await source.getClusterExpansionZoom(clusterId);\n\t\tmapInstance.easeTo({\n\t\t\tcenter: (feature.geometry as Point).coordinates as [number, number],\n\t\t\tzoom\n\t\t});\n\t};\n\n\tconst handleClusterMouseEnter = (_event: MapLayerMouseEvent, mapInstance: maplibregl.Map) => {\n\t\tmapInstance.getCanvas().style.cursor = 'pointer';\n\t};\n\n\tconst handleClusterMouseLeave = (_event: MapLayerMouseEvent, mapInstance: maplibregl.Map) => {\n\t\tmapInstance.getCanvas().style.cursor = '';\n\t};\n\n\tconst handleVehicleMouseMove = createMouseMoveHandler(lookupVehicle, (provider, entity) =>\n\t\tcreateVehicleContent(provider, entity as RentalVehicle, true)\n\t);\n\tconst handleVehicleMouseLeave = createMouseLeaveHandler();\n\tconst handleVehicleClick = createClickHandler(lookupVehicle, (provider, entity) =>\n\t\tcreateVehicleContent(provider, entity as RentalVehicle, true)\n\t);\n\n\tconst handleZoneClick = (\n\t\tevent: maplibregl.MapMouseEvent,\n\t\tmapInstance: maplibregl.Map,\n\t\tlayer: ZoneLayer\n\t) => {\n\t\tif (!mapInstance) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if there's a station or vehicle at this location (they should take priority)\n\t\tconst priorityLayers = [\n\t\t\tSTATION_ICON_LAYER_ID,\n\t\t\t...vehicleLayerConfigs.map((c) => c.pointLayerId),\n\t\t\t...vehicleLayerConfigs.map((c) => c.clusterLayerId)\n\t\t];\n\t\tconst priorityFeatures = mapInstance.queryRenderedFeatures(event.point, {\n\t\t\tlayers: priorityLayers\n\t\t});\n\t\tif (priorityFeatures.length > 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst feature = layer.pick(event.point);\n\t\tif (!feature) {\n\t\t\thidePopup();\n\t\t\treturn;\n\t\t}\n\n\t\tconst result = lookupZoneOrStation(feature.properties);\n\t\tif ((!result.zone && !result.station) || !result.provider) {\n\t\t\thidePopup();\n\t\t\treturn;\n\t\t}\n\n\t\tconst allZonesAtPoint = zoneRTree\n\t\t\t.search({\n\t\t\t\tminX: event.lngLat.lng,\n\t\t\t\tminY: event.lngLat.lat,\n\t\t\t\tmaxX: event.lngLat.lng,\n\t\t\t\tmaxY: event.lngLat.lat\n\t\t\t})\n\t\t\t.map((item) => item.feature)\n\t\t\t.filter((feature) =>\n\t\t\t\tbooleanPointInPolygon(point([event.lngLat.lng, event.lngLat.lat]), feature)\n\t\t\t)\n\t\t\t.sort((a, b) => b.properties.z - a.properties.z);\n\t\thideTooltip();\n\t\thidePopup();\n\t\tshowPopup(mapInstance, event.lngLat, result.key, () =>\n\t\t\tcreateZoneContent(\n\t\t\t\tresult.provider!,\n\t\t\t\tresult.zone,\n\t\t\t\tresult.station,\n\t\t\t\tresult.rideThroughAllowed,\n\t\t\t\tresult.rideEndAllowed,\n\t\t\t\tallZonesAtPoint\n\t\t\t)\n\t\t);\n\t};\n\n\t$effect(() => {\n\t\tif (!map || !bounds || zoom <= MIN_ZOOM) {\n\t\t\tif (zoom <= MIN_ZOOM) {\n\t\t\t\trentalsData = null;\n\t\t\t\tloadedBounds = null;\n\t\t\t\trequestToken += 1;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst mapBounds = maplibregl.LngLatBounds.convert(bounds);\n\t\tif (boundsContain(loadedBounds, mapBounds)) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid fetchRentals(mapBounds);\n\t});\n\n\t$effect(() => {\n\t\tif (\n\t\t\tdisplayFilter &&\n\t\t\t!providerGroupOptions.some((option) => isSameFilter(option, displayFilter))\n\t\t) {\n\t\t\tdisplayFilter = null;\n\t\t}\n\t});\n\n\tconst clearMapIcons = (mapInstance: maplibregl.Map | undefined) => {\n\t\tif (!mapInstance || activeIconIds.size === 0) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const id of activeIconIds) {\n\t\t\tif (mapInstance.hasImage(id)) {\n\t\t\t\tmapInstance.removeImage(id);\n\t\t\t}\n\t\t}\n\t\tactiveIconIds = new SvelteSet<string>();\n\t};\n\n\t$effect(() => {\n\t\tconst mapInstance = map;\n\t\tconst data = rentalsData;\n\t\tif (!browser || !mapInstance) {\n\t\t\ticonsReady = false;\n\t\t\ticonRequestToken += 1;\n\t\t\tclearMapIcons(mapInstance);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!data) {\n\t\t\ticonsReady = false;\n\t\t\ticonRequestToken += 1;\n\t\t\tclearMapIcons(mapInstance);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dataPayload = data as RentalsPayloadData;\n\t\tconst formFactorsByColor = collectFormFactorsByColor(dataPayload);\n\t\tif (formFactorsByColor.size === 0) {\n\t\t\ticonRequestToken += 1;\n\t\t\tclearMapIcons(mapInstance);\n\t\t\ticonsReady = true;\n\t\t\treturn;\n\t\t}\n\n\t\tconst token = ++iconRequestToken;\n\t\ticonsReady = false;\n\t\t/* eslint-disable-next-line svelte/prefer-svelte-reactivity */\n\t\tconst neededIds = new Set<string>();\n\t\tconst tasks: Promise<void>[] = [];\n\n\t\tformFactorsByColor.forEach((formFactors, color) => {\n\t\t\tformFactors.forEach((formFactor) => {\n\t\t\t\tICON_TYPES.forEach((type) => {\n\t\t\t\t\tconst dimensions = getIconDimensions(type);\n\t\t\t\t\tconst id = getIconId(formFactor, type, color);\n\t\t\t\t\tneededIds.add(id);\n\t\t\t\t\tif (!mapInstance.hasImage(id)) {\n\t\t\t\t\t\ttasks.push(\n\t\t\t\t\t\t\t(async () => {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst image = await colorizeIcon(getIconUrl(formFactor, type), color, dimensions);\n\t\t\t\t\t\t\t\t\tif (token !== iconRequestToken || !mapInstance) {\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif (!mapInstance.hasImage(id)) {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\tmapInstance.addImage(id, image);\n\t\t\t\t\t\t\t\t\t\t} catch (addError) {\n\t\t\t\t\t\t\t\t\t\t\tconsole.error(`[Rentals] failed to register icon ${id}`, addError);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\t\tconsole.error(`[Rentals] failed to prepare icon ${id}`, error);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t})()\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\n\t\tvoid (async () => {\n\t\t\tawait Promise.allSettled(tasks);\n\t\t\tif (token !== iconRequestToken || !mapInstance) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst previousIds = Array.from(activeIconIds);\n\t\t\tfor (const id of previousIds) {\n\t\t\t\tif (!neededIds.has(id) && mapInstance.hasImage(id)) {\n\t\t\t\t\tmapInstance.removeImage(id);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst newActiveIds = new SvelteSet<string>();\n\t\t\tneededIds.forEach((id) => {\n\t\t\t\tif (mapInstance.hasImage(id)) {\n\t\t\t\t\tnewActiveIds.add(id);\n\t\t\t\t}\n\t\t\t});\n\t\t\tactiveIconIds = newActiveIds;\n\t\t\ticonsReady = neededIds.size === newActiveIds.size;\n\t\t})();\n\t});\n\n\t$effect(() => {\n\t\tconst mapInstance = map;\n\t\tconst layer = zoneLayerRef;\n\t\tconst hasZones = zoneFeatures.features.length > 0;\n\t\tif (!mapInstance || !layer || !hasZones) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handler = (event: maplibregl.MapMouseEvent) => {\n\t\t\thandleZoneClick(event, mapInstance, layer);\n\t\t};\n\t\tmapInstance.on('click', handler);\n\t\treturn () => {\n\t\t\tmapInstance.off('click', handler);\n\t\t};\n\t});\n\n\t$effect(() => {\n\t\tif (!rentalsData) {\n\t\t\thideTooltip();\n\t\t\thidePopup();\n\t\t\tresetHoverCursor();\n\t\t\treturn;\n\t\t}\n\t\tif (\n\t\t\tactiveTooltipKey &&\n\t\t\t!stationById.has(activeTooltipKey) &&\n\t\t\t!vehicleById.has(activeTooltipKey)\n\t\t) {\n\t\t\thideTooltip();\n\t\t}\n\t\tif (activePopupKey && !stationById.has(activePopupKey) && !vehicleById.has(activePopupKey)) {\n\t\t\thidePopup();\n\t\t}\n\t});\n\n\tonDestroy(() => {\n\t\trequestToken += 1;\n\t\thideTooltip();\n\t\thidePopup();\n\t\tresetHoverCursor();\n\t\tclearMapIcons(map);\n\t\ttooltipPopup = null;\n\t\tdetailPopup = null;\n\t});\n</script>\n\n{#if zoneFeatures.features.length > 0}\n\t{@const beforeLayerId = vehicleLayerConfigs[0]?.pointLayerId ?? STATION_ICON_LAYER_ID}\n\t<ZoneLayer\n\t\tbind:this={zoneLayerRef}\n\t\tid={ZONE_LAYER_ID}\n\t\tfeatures={zoneFeatures.features}\n\t\t{beforeLayerId}\n\t\topacity={theme === 'dark' ? 0.3 : 0.4}\n\t/>\n{/if}\n\n<GeoJSON id={STATION_SOURCE_ID} data={stationFeatures}>\n\t<Layer\n\t\tid={STATION_ICON_LAYER_ID}\n\t\tbeforeLayerId=\"\"\n\t\ttype=\"symbol\"\n\t\tfilter={true}\n\t\tlayout={{\n\t\t\t'icon-image': ['get', 'icon'],\n\t\t\t'icon-size': zoomScaledIconSize,\n\t\t\t'icon-allow-overlap': false,\n\t\t\t'icon-ignore-placement': true,\n\t\t\t'text-field': ['to-string', ['get', 'available']],\n\t\t\t'text-allow-overlap': false,\n\t\t\t'text-ignore-placement': true,\n\t\t\t'text-anchor': 'center',\n\t\t\t'text-offset': zoomScaledTextOffset,\n\t\t\t'text-size': zoomScaledTextSizeMedium,\n\t\t\t'text-font': ['Noto Sans Regular']\n\t\t}}\n\t\tonmousemove={handleStationMouseMove}\n\t\tonmouseleave={handleStationMouseLeave}\n\t\tonclick={handleStationClick}\n\t\tpaint={{\n\t\t\t'text-color': '#000'\n\t\t}}\n\t/>\n</GeoJSON>\n\n{#each vehicleLayerConfigs as config (config.sourceId)}\n\t<GeoJSON\n\t\tid={config.sourceId}\n\t\tdata={vehicleCollections[config.formFactor]}\n\t\toptions={{\n\t\t\tcluster: true,\n\t\t\tclusterRadius: VEHICLE_CLUSTER_RADIUS,\n\t\t\tclusterProperties: {\n\t\t\t\tcolor: ['coalesce', ['get', 'color']],\n\t\t\t\ticon: ['coalesce', ['get', 'icon']],\n\t\t\t\tclusterIcon: ['coalesce', ['get', 'clusterIcon']]\n\t\t\t}\n\t\t}}\n\t>\n\t\t<Layer\n\t\t\tid={config.clusterLayerId}\n\t\t\tbeforeLayerId={STATION_ICON_LAYER_ID}\n\t\t\ttype=\"symbol\"\n\t\t\tfilter={['has', 'point_count']}\n\t\t\tlayout={{\n\t\t\t\t'icon-image': ['get', 'clusterIcon'],\n\t\t\t\t'icon-size': zoomScaledIconSize,\n\t\t\t\t'icon-allow-overlap': true,\n\t\t\t\t'icon-ignore-placement': true,\n\t\t\t\t'text-field': ['to-string', ['get', 'point_count']],\n\t\t\t\t'text-allow-overlap': true,\n\t\t\t\t'text-ignore-placement': true,\n\t\t\t\t'text-anchor': 'center',\n\t\t\t\t'text-offset': zoomScaledTextOffset,\n\t\t\t\t'text-size': zoomScaledTextSizeSmall,\n\t\t\t\t'text-font': ['Noto Sans Regular']\n\t\t\t}}\n\t\t\tpaint={{\n\t\t\t\t'text-color': '#000'\n\t\t\t}}\n\t\t\tonclick={handleClusterClick}\n\t\t\tonmouseenter={handleClusterMouseEnter}\n\t\t\tonmouseleave={handleClusterMouseLeave}\n\t\t/>\n\t\t<Layer\n\t\t\tid={config.pointLayerId}\n\t\t\tbeforeLayerId={config.clusterLayerId}\n\t\t\ttype=\"symbol\"\n\t\t\tfilter={['!', ['has', 'point_count']]}\n\t\t\tlayout={{\n\t\t\t\t'icon-image': ['get', 'icon'],\n\t\t\t\t'icon-size': zoomScaledIconSize,\n\t\t\t\t'icon-allow-overlap': true,\n\t\t\t\t'icon-ignore-placement': true\n\t\t\t}}\n\t\t\tpaint={{}}\n\t\t\tonmousemove={handleVehicleMouseMove}\n\t\t\tonmouseleave={handleVehicleMouseLeave}\n\t\t\tonclick={handleVehicleClick}\n\t\t/>\n\t</GeoJSON>\n{/each}\n\n{#if providerGroupOptions.length > 0}\n\t<Control position=\"top-right\" class=\"mb-5\">\n\t\t<div class=\"flex flex-col items-end space-y-2\">\n\t\t\t{#each providerGroupOptions as option (`${option.providerGroupId}::${option.formFactor}`)}\n\t\t\t\t{@const active = isSameFilter(displayFilter, option)}\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\ttitle={`${option.providerGroupName} (${formFactorAssets[option.formFactor].label})`}\n\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t'inline-flex max-w-72 items-center gap-2 rounded-md border-2 px-3 py-1.5 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500',\n\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t? 'border-blue-600 bg-accent text-accent-foreground'\n\t\t\t\t\t\t\t: 'border-muted bg-popover text-foreground hover:bg-accent hover:text-accent-foreground'\n\t\t\t\t\t)}\n\t\t\t\t\tonclick={() => toggleFilter(option)}\n\t\t\t\t\taria-pressed={active}\n\t\t\t\t>\n\t\t\t\t\t<span class=\"truncate\">{option.providerGroupName}</span>\n\t\t\t\t\t<span class=\"flex items-center gap-1 text-xs font-medium\">\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\tclass=\"h-4 w-4 fill-current\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\tfocusable=\"false\"\n\t\t\t\t\t\t\tstyle={`color: ${option.color}`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<use href={`#${formFactorAssets[option.formFactor].svg}`} />\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t</button>\n\t\t\t{/each}\n\t\t</div>\n\t</Control>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/map/rentals/StationPopup.svelte",
    "content": "<script lang=\"ts\">\n\timport type {\n\t\tRentalFormFactor,\n\t\tRentalProvider,\n\t\tRentalStation\n\t} from '@motis-project/motis-client';\n\timport { Button } from '$lib/components/ui/button';\n\timport { Copy, type Icon as IconType } from '@lucide/svelte';\n\timport { formFactorAssets, propulsionTypes, returnConstraints } from '$lib/map/rentals/assets';\n\timport { t } from '$lib/i18n/translation';\n\n\tlet {\n\t\tprovider,\n\t\tstation,\n\t\tshowActions = false,\n\t\tdebug = false\n\t}: {\n\t\tprovider: RentalProvider;\n\t\tstation: RentalStation;\n\t\tshowActions?: boolean;\n\t\tdebug?: boolean;\n\t} = $props();\n\n\tlet debugInfo = $derived({\n\t\tstation,\n\t\tprovider: {\n\t\t\t...provider,\n\t\t\tvehicleTypes: provider.vehicleTypes.filter(\n\t\t\t\t(vt) =>\n\t\t\t\t\tObject.hasOwn(station.vehicleTypesAvailable, vt.id) ||\n\t\t\t\t\tObject.hasOwn(station.vehicleDocksAvailable, vt.id)\n\t\t\t),\n\t\t\ttotalVehicleTypes: provider.vehicleTypes.length\n\t\t}\n\t});\n\n\tasync function copyDebugInfo() {\n\t\tawait navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));\n\t}\n\n\ttype IconInfo = {\n\t\tcomponent: typeof IconType;\n\t\ttitle: string;\n\t};\n\n\ttype VehicleRow = {\n\t\tid: string;\n\t\tavailable: number;\n\t\tformFactor: RentalFormFactor;\n\t\tname: string;\n\t\tpropulsionIcon: IconInfo | null;\n\t\treturnIcon: IconInfo | null;\n\t};\n\n\tconst vehicleRows = $derived.by<VehicleRow[]>(() => {\n\t\treturn Object.entries(station.vehicleTypesAvailable)\n\t\t\t.map(([id, count]) => {\n\t\t\t\tconst vt = provider.vehicleTypes.find((vt) => vt.id === id)!;\n\t\t\t\tconst name = vt.name || formFactorAssets[vt.formFactor].label;\n\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tavailable: count,\n\t\t\t\t\tformFactor: vt.formFactor,\n\t\t\t\t\tname,\n\t\t\t\t\tpropulsionIcon: propulsionTypes[vt.propulsionType],\n\t\t\t\t\treturnIcon: returnConstraints[vt.returnConstraint]\n\t\t\t\t};\n\t\t\t})\n\t\t\t.sort((a, b) => {\n\t\t\t\tif (b.available !== a.available) {\n\t\t\t\t\treturn b.available - a.available;\n\t\t\t\t}\n\t\t\t\treturn a.name.localeCompare(b.name);\n\t\t\t});\n\t});\n</script>\n\n<div class=\"space-y-3 text-sm leading-tight text-foreground max-w-96 w-fit\">\n\t<div class=\"space-y-1\">\n\t\t<div class=\"font-semibold\">{station.name}</div>\n\t\t{#if station.address}\n\t\t\t<div>{station.address}</div>\n\t\t{/if}\n\t\t<div>\n\t\t\t{t.sharingProvider}: {#if provider.url}\n\t\t\t\t<a\n\t\t\t\t\thref={provider.url}\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\tclass=\"text-blue-600 dark:text-blue-300 hover:underline\"\n\t\t\t\t>\n\t\t\t\t\t{provider.name}\n\t\t\t\t</a>\n\t\t\t{:else}\n\t\t\t\t{provider.name}\n\t\t\t{/if}\n\t\t</div>\n\t</div>\n\t{#if vehicleRows.length}\n\t\t<table class=\"w-full text-xs\">\n\t\t\t<tbody>\n\t\t\t\t{#each vehicleRows as vehicle (vehicle.id)}\n\t\t\t\t\t<tr class=\"border-b border-border last:border-0\">\n\t\t\t\t\t\t<td class=\"w-8 pr-2 align-middle\">\n\t\t\t\t\t\t\t{vehicle.available}x\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"w-6 pr-2 align-middle\">\n\t\t\t\t\t\t\t<svg class=\"h-4 w-4 fill-current\" aria-hidden=\"true\" focusable=\"false\">\n\t\t\t\t\t\t\t\t<title>{formFactorAssets[vehicle.formFactor].label}</title>\n\t\t\t\t\t\t\t\t<use href={`#${formFactorAssets[vehicle.formFactor].svg}`} />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"w-6 pr-2 align-middle\">\n\t\t\t\t\t\t\t{#if vehicle.propulsionIcon}\n\t\t\t\t\t\t\t\t{@const PropulsionIcon = vehicle.propulsionIcon.component}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"inline-flex h-4 w-4 items-center justify-center text-muted-foreground\"\n\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\ttitle={vehicle.propulsionIcon.title}\n\t\t\t\t\t\t\t\t\taria-label={vehicle.propulsionIcon.title}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<PropulsionIcon class=\"h-4 w-4\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tclass=\"truncate align-middle max-w-64\"\n\t\t\t\t\t\t\ttitle={vehicle.name}\n\t\t\t\t\t\t\taria-label={vehicle.name}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{vehicle.name}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"w-8 pl-2 align-middle text-right\">\n\t\t\t\t\t\t\t{#if vehicle.returnIcon}\n\t\t\t\t\t\t\t\t{@const ReturnIcon = vehicle.returnIcon.component}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"inline-flex h-4 w-4 items-center justify-center text-muted-foreground\"\n\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\ttitle={vehicle.returnIcon.title}\n\t\t\t\t\t\t\t\t\taria-label={vehicle.returnIcon.title}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<ReturnIcon class=\"h-4 w-4\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t{/each}\n\t\t\t</tbody>\n\t\t</table>\n\t{/if}\n\t{#if showActions && station.rentalUriWeb}\n\t\t<Button class=\"font-bold\" variant=\"outline\" href={station.rentalUriWeb} target=\"_blank\">\n\t\t\t{t.rent}\n\t\t</Button>\n\t{/if}\n\t{#if debug}\n\t\t<div\n\t\t\tclass=\"pt-2 border-t border-border text-xs text-muted-foreground space-y-1 max-h-96 max-w-96 overflow-auto pr-2 relative\"\n\t\t>\n\t\t\t<Button\n\t\t\t\tclass=\"absolute top-2 right-2 z-10\"\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonclick={copyDebugInfo}\n\t\t\t\ttype=\"button\"\n\t\t\t\ttitle={t.copyToClipboard}\n\t\t\t\taria-label={t.copyToClipboard}\n\t\t\t>\n\t\t\t\t<Copy />\n\t\t\t</Button>\n\t\t\t<pre class=\"whitespace-pre-wrap pr-8\">{JSON.stringify(debugInfo, null, 2)}</pre>\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/map/rentals/VehiclePopup.svelte",
    "content": "<script lang=\"ts\">\n\timport type { RentalProvider, RentalVehicle } from '@motis-project/motis-client';\n\timport { Button } from '$lib/components/ui/button';\n\timport { Copy } from '@lucide/svelte';\n\timport { t } from '$lib/i18n/translation';\n\timport { formFactorAssets, propulsionTypes, returnConstraints } from './assets';\n\n\tlet {\n\t\tprovider,\n\t\tvehicle,\n\t\tshowActions = false,\n\t\tdebug = false\n\t}: {\n\t\tprovider: RentalProvider;\n\t\tvehicle: RentalVehicle;\n\t\tshowActions?: boolean;\n\t\tdebug?: boolean;\n\t} = $props();\n\n\tlet vehicleType = $derived.by(() => provider.vehicleTypes.find((vt) => vt.id == vehicle.typeId));\n\tlet propulsion = $derived(propulsionTypes[vehicle.propulsionType]);\n\tlet returnConstraint = $derived(returnConstraints[vehicle.returnConstraint]);\n\n\tlet debugInfo = $derived({\n\t\tvehicle,\n\t\tprovider: {\n\t\t\t...provider,\n\t\t\tvehicleTypes: provider.vehicleTypes.filter((vt) => vt.id == vehicle.typeId),\n\t\t\ttotalVehicleTypes: provider.vehicleTypes.length\n\t\t}\n\t});\n\n\tasync function copyDebugInfo() {\n\t\tawait navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));\n\t}\n</script>\n\n<div class=\"space-y-3 text-sm leading-tight text-foreground max-w-80\">\n\t<div class=\"flex gap-2\">\n\t\t<svg class=\"h-4 w-4 fill-current flex-none\" aria-hidden=\"true\" focusable=\"false\">\n\t\t\t<title>{formFactorAssets[vehicle.formFactor].label}</title>\n\t\t\t<use href={`#${formFactorAssets[vehicle.formFactor].svg}`} />\n\t\t</svg>\n\t\t{#if propulsion}\n\t\t\t<span\n\t\t\t\tclass=\"inline-flex h-4 w-4 items-center justify-center text-muted-foreground\"\n\t\t\t\trole=\"img\"\n\t\t\t\ttitle={propulsion.title}\n\t\t\t\taria-label={propulsion.title}\n\t\t\t>\n\t\t\t\t<propulsion.component class=\"h-4 w-4\" aria-hidden=\"true\" />\n\t\t\t</span>\n\t\t{/if}\n\t\t<span>{vehicleType?.name || formFactorAssets[vehicle.formFactor].label}</span>\n\t</div>\n\t<div>\n\t\t{t.sharingProvider}:\n\t\t{#if provider.url}\n\t\t\t<a\n\t\t\t\thref={provider.url}\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tclass=\"text-blue-600 dark:text-blue-300 hover:underline\"\n\t\t\t>\n\t\t\t\t{provider.name}\n\t\t\t</a>\n\t\t{:else}\n\t\t\t{provider.name}\n\t\t{/if}\n\t</div>\n\t{#if returnConstraint}\n\t\t<div class=\"flex gap-2\">\n\t\t\t<span\n\t\t\t\tclass=\"inline-flex h-4 w-4 items-center justify-center text-muted-foreground flex-none\"\n\t\t\t\trole=\"img\"\n\t\t\t\ttitle={returnConstraint.title}\n\t\t\t\taria-label={returnConstraint.title}\n\t\t\t>\n\t\t\t\t<returnConstraint.component class=\"h-4 w-4\" aria-hidden=\"true\" />\n\t\t\t</span>\n\t\t\t<span>{returnConstraint.title}</span>\n\t\t</div>\n\t{/if}\n\t{#if showActions && vehicle.rentalUriWeb}\n\t\t<Button class=\"font-bold\" variant=\"outline\" href={vehicle.rentalUriWeb} target=\"_blank\">\n\t\t\t{t.rent}\n\t\t</Button>\n\t{/if}\n\t{#if debug}\n\t\t<div\n\t\t\tclass=\"pt-2 border-t border-border text-xs text-muted-foreground space-y-1 max-h-96 max-w-96 overflow-auto pr-2 relative\"\n\t\t>\n\t\t\t<Button\n\t\t\t\tclass=\"absolute top-2 right-2 z-10\"\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonclick={copyDebugInfo}\n\t\t\t\ttype=\"button\"\n\t\t\t\ttitle={t.copyToClipboard}\n\t\t\t\taria-label={t.copyToClipboard}\n\t\t\t>\n\t\t\t\t<Copy />\n\t\t\t</Button>\n\t\t\t<pre class=\"whitespace-pre-wrap pr-8\">{JSON.stringify(debugInfo, null, 2)}</pre>\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/map/rentals/ZoneLayer.svelte",
    "content": "<script lang=\"ts\">\n\timport maplibregl from 'maplibre-gl';\n\timport type { PointLike } from 'maplibre-gl';\n\timport { getContext, onDestroy } from 'svelte';\n\n\timport { ZoneFillLayer } from './zone-fill-layer';\n\timport type { RentalZoneFeature } from './zone-types';\n\n\tclass Props {\n\t\tid!: string;\n\t\tfeatures!: RentalZoneFeature[];\n\t\tbeforeLayerId?: string;\n\t\topacity?: number;\n\t}\n\n\tconst ZONE_FILL_OPACITY = 0.4;\n\n\tlet { id, features, beforeLayerId, opacity = ZONE_FILL_OPACITY }: Props = $props();\n\n\ttype MapContext = { map: maplibregl.Map | null };\n\tconst ctx: MapContext = getContext('map');\n\n\tlet layerInstance = $state<ZoneFillLayer | null>(null);\n\tlet pendingRetryHandler: ((event?: unknown) => void) | null = null;\n\tlet currentFeatures: RentalZoneFeature[] | null = null;\n\tlet lastOpacity: number | undefined;\n\n\tconst ensureLayerInstance = () => {\n\t\tif (!layerInstance) {\n\t\t\tlayerInstance = new ZoneFillLayer({ id, opacity });\n\t\t}\n\t\treturn layerInstance;\n\t};\n\n\tconst disposeLayerInstance = () => {\n\t\tif (!layerInstance) {\n\t\t\treturn;\n\t\t}\n\t\tlayerInstance.cleanup();\n\t\tlayerInstance = null;\n\t\tcurrentFeatures = null;\n\t};\n\n\tconst getLayerIndex = (mapInstance: maplibregl.Map, layerId: string) => {\n\t\tconst style = mapInstance.getStyle();\n\t\tconst layers = style?.layers ?? [];\n\t\treturn layers.findIndex((layer) => layer.id === layerId);\n\t};\n\n\tconst clearPendingRetry = (map: maplibregl.Map | null | undefined) => {\n\t\tif (!map || !pendingRetryHandler) {\n\t\t\treturn;\n\t\t}\n\t\tmap.off('styledata', pendingRetryHandler);\n\t\tmap.off('idle', pendingRetryHandler);\n\t\tpendingRetryHandler = null;\n\t};\n\n\tconst removeFromMap = (map: maplibregl.Map | null | undefined) => {\n\t\tconst instance = layerInstance;\n\t\tif (!instance) {\n\t\t\treturn;\n\t\t}\n\t\tif (map && map.getLayer(id)) {\n\t\t\ttry {\n\t\t\t\tmap.removeLayer(id);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[ZoneLayer] failed to remove layer', error);\n\t\t\t}\n\t\t}\n\t\tinstance.setFeatures([]);\n\t\tdisposeLayerInstance();\n\t};\n\n\t$effect(() => {\n\t\tconst map = ctx.map;\n\t\tif (!map || features.length === 0) {\n\t\t\tclearPendingRetry(map);\n\t\t\tremoveFromMap(map);\n\t\t\tcurrentFeatures = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst layer = ensureLayerInstance();\n\t\tif (lastOpacity !== opacity) {\n\t\t\tlayer.setOpacity(opacity);\n\t\t\tlastOpacity = opacity;\n\t\t}\n\t\tif (features !== currentFeatures) {\n\t\t\tcurrentFeatures = features;\n\t\t\tlayer.setFeatures(features);\n\t\t}\n\n\t\tconst ensureLayerOrder = () => {\n\t\t\tif (!beforeLayerId || beforeLayerId === id) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!map.getLayer(id) || !map.getLayer(beforeLayerId)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst layerIndex = getLayerIndex(map, id);\n\t\t\tconst targetIndex = getLayerIndex(map, beforeLayerId);\n\t\t\tif (layerIndex === -1 || targetIndex === -1) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (layerIndex >= targetIndex) {\n\t\t\t\tmap.moveLayer(id, beforeLayerId);\n\t\t\t}\n\t\t};\n\n\t\tconst addLayerIfNeeded = () => {\n\t\t\tif (!map.getLayer(id)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst before = beforeLayerId && map.getLayer(beforeLayerId) ? beforeLayerId : undefined;\n\t\t\t\t\tif (before) {\n\t\t\t\t\t\tmap.addLayer(layer, before);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmap.addLayer(layer);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error('[ZoneLayer] failed to add layer', error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tensureLayerOrder();\n\t\t};\n\n\t\tconst attemptAddLayer = () => {\n\t\t\tif (!map.isStyleLoaded()) {\n\t\t\t\tif (!pendingRetryHandler) {\n\t\t\t\t\tconst retry = () => {\n\t\t\t\t\t\tif (!pendingRetryHandler) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmap.off('styledata', retry);\n\t\t\t\t\t\tmap.off('idle', retry);\n\t\t\t\t\t\tpendingRetryHandler = null;\n\t\t\t\t\t\tattemptAddLayer();\n\t\t\t\t\t};\n\t\t\t\t\tpendingRetryHandler = retry;\n\t\t\t\t\tmap.on('styledata', retry);\n\t\t\t\t\tmap.on('idle', retry);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\taddLayerIfNeeded();\n\t\t};\n\n\t\tconst handleStyleData = () => {\n\t\t\tattemptAddLayer();\n\t\t\tensureLayerOrder();\n\t\t};\n\n\t\tattemptAddLayer();\n\t\tmap.on('styledata', handleStyleData);\n\n\t\treturn () => {\n\t\t\tmap.off('styledata', handleStyleData);\n\t\t\tclearPendingRetry(map);\n\t\t};\n\t});\n\n\tonDestroy(() => {\n\t\tconst mapInstance = ctx.map;\n\t\tclearPendingRetry(mapInstance);\n\t\tremoveFromMap(mapInstance);\n\t\tdisposeLayerInstance();\n\t\tlastOpacity = opacity;\n\t});\n\n\texport function pick(point: PointLike) {\n\t\treturn layerInstance?.pickFeatureAt(point) ?? null;\n\t}\n</script>\n"
  },
  {
    "path": "ui/src/lib/map/rentals/ZonePopup.svelte",
    "content": "<script lang=\"ts\">\n\timport type { RentalProvider, RentalStation, RentalZone } from '@motis-project/motis-client';\n\timport { Button } from '$lib/components/ui/button';\n\timport { Copy } from '@lucide/svelte';\n\timport { t } from '$lib/i18n/translation';\n\timport type { RentalZoneFeature } from './zone-types';\n\n\tlet {\n\t\tprovider,\n\t\tzone,\n\t\tstation,\n\t\trideThroughAllowed,\n\t\trideEndAllowed,\n\t\tallZonesAtPoint = [],\n\t\tzoneData = [],\n\t\tstationData = [],\n\t\tdebug = false\n\t}: {\n\t\tprovider: RentalProvider;\n\t\tzone: RentalZone | undefined;\n\t\tstation: RentalStation | undefined;\n\t\trideThroughAllowed: boolean;\n\t\trideEndAllowed: boolean;\n\t\tallZonesAtPoint?: RentalZoneFeature[];\n\t\tzoneData?: RentalZone[];\n\t\tstationData?: RentalStation[];\n\t\tdebug?: boolean;\n\t} = $props();\n\n\tlet debugInfo = $derived({\n\t\t...(zone ? { zone: { ...zone, area: '...' } } : {}),\n\t\t...(station ? { station: { ...station, stationArea: '...' } } : {}),\n\t\tprovider: provider,\n\t\tallZonesAtPoint: allZonesAtPoint.map((feature) => ({\n\t\t\tz: feature.properties.z,\n\t\t\tzoneIndex: feature.properties.zoneIndex,\n\t\t\tstationIndex: feature.properties.stationIndex,\n\t\t\trideThroughAllowed: feature.properties.rideThroughAllowed,\n\t\t\trideEndAllowed: feature.properties.rideEndAllowed,\n\t\t\tstationArea: feature.properties.stationArea,\n\t\t\tzone: (() => {\n\t\t\t\tif (feature.properties.zoneIndex === undefined) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst zone = zoneData[feature.properties.zoneIndex];\n\t\t\t\treturn zone ? { ...zone, area: '...' } : null;\n\t\t\t})(),\n\t\t\tstation: (() => {\n\t\t\t\tif (feature.properties.stationIndex === undefined) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst station = stationData[feature.properties.stationIndex];\n\t\t\t\treturn station ? { ...station, stationArea: '...' } : null;\n\t\t\t})()\n\t\t}))\n\t});\n\n\tasync function copyDebugInfo() {\n\t\tawait navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));\n\t}\n</script>\n\n<div class=\"space-y-3 text-sm leading-tight text-foreground\">\n\t<div class=\"space-y-1\">\n\t\t{#if zone}\n\t\t\t{#if zone.name}\n\t\t\t\t<div class=\"font-semibold\">{t.rentalGeofencingZone}: {zone.name}</div>\n\t\t\t{:else}\n\t\t\t\t<div class=\"font-semibold\">{t.rentalGeofencingZone}</div>\n\t\t\t{/if}\n\t\t{:else if station}\n\t\t\t{#if station.name}\n\t\t\t\t<div class=\"font-semibold\">{t.rentalStation}: {station.name}</div>\n\t\t\t{:else}\n\t\t\t\t<div class=\"font-semibold\">{t.rentalStation}</div>\n\t\t\t{/if}\n\t\t{/if}\n\t\t<div>\n\t\t\t{t.sharingProvider}: {#if provider.url}\n\t\t\t\t<a\n\t\t\t\t\thref={provider.url}\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\tclass=\"text-blue-600 dark:text-blue-300 hover:underline\"\n\t\t\t\t>\n\t\t\t\t\t{provider.name}\n\t\t\t\t</a>\n\t\t\t{:else}\n\t\t\t\t{provider.name}\n\t\t\t{/if}\n\t\t</div>\n\t</div>\n\t<div class=\"space-y-1\">\n\t\t<div>\n\t\t\t{rideThroughAllowed ? t.rideThroughAllowed : t.rideThroughNotAllowed}\n\t\t</div>\n\t\t{#if rideThroughAllowed}\n\t\t\t<div>\n\t\t\t\t{rideEndAllowed ? t.rideEndAllowed : t.rideEndNotAllowed}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n\t{#if debug}\n\t\t<div\n\t\t\tclass=\"pt-2 border-t border-border text-xs text-muted-foreground space-y-1 max-h-96 max-w-96 overflow-auto pr-2 relative\"\n\t\t>\n\t\t\t<Button\n\t\t\t\tclass=\"absolute top-2 right-2 z-10\"\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"icon\"\n\t\t\t\tonclick={copyDebugInfo}\n\t\t\t\ttype=\"button\"\n\t\t\t\ttitle={t.copyToClipboard}\n\t\t\t\taria-label={t.copyToClipboard}\n\t\t\t>\n\t\t\t\t<Copy />\n\t\t\t</Button>\n\t\t\t<pre class=\"whitespace-pre-wrap pr-8\">{JSON.stringify(debugInfo, null, 2)}</pre>\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "ui/src/lib/map/rentals/assets.ts",
    "content": "import { browser } from '$app/environment';\nimport type {\n\tRentalFormFactor,\n\tRentalPropulsionType,\n\tRentalReturnConstraint\n} from '@motis-project/motis-client';\nimport {\n\tFlagTriangleLeft,\n\tFuel,\n\tPlugZap,\n\tRefreshCcw,\n\tZap,\n\ttype Icon as LucideIcon\n} from '@lucide/svelte';\nimport { t } from '$lib/i18n/translation';\n\nexport type FormFactorAssets = {\n\tsvg: string;\n\tstation: string;\n\tvehicle: string;\n\tcluster: string;\n\tlabel: string;\n};\n\ntype IconDimensions = {\n\twidth: number;\n\theight: number;\n};\n\nexport type MapLibreImageSource = ImageBitmap | HTMLImageElement;\n\nexport const DEFAULT_FORM_FACTOR: RentalFormFactor = 'BICYCLE';\n\nexport const ICON_TYPES = ['station', 'vehicle', 'cluster'] as const;\nexport type IconType = (typeof ICON_TYPES)[number];\n\nexport const ICON_BASE_PATH = 'icons/rental/';\n\nexport const formFactorAssets: Record<RentalFormFactor, FormFactorAssets> = {\n\tBICYCLE: {\n\t\tsvg: 'bike',\n\t\tstation: 'bike_station',\n\t\tvehicle: 'floating_bike',\n\t\tcluster: 'floating_bike_cluster',\n\t\tlabel: t.bike\n\t},\n\tCARGO_BICYCLE: {\n\t\tsvg: 'cargo_bike',\n\t\tstation: 'cargo_bike_station',\n\t\tvehicle: 'floating_cargo_bike',\n\t\tcluster: 'floating_cargo_bike_cluster',\n\t\tlabel: t.cargoBike\n\t},\n\tCAR: {\n\t\tsvg: 'car',\n\t\tstation: 'car_station',\n\t\tvehicle: 'floating_car',\n\t\tcluster: 'floating_car_cluster',\n\t\tlabel: t.car\n\t},\n\tMOPED: {\n\t\tsvg: 'moped',\n\t\tstation: 'moped_station',\n\t\tvehicle: 'floating_moped',\n\t\tcluster: 'floating_moped_cluster',\n\t\tlabel: t.moped\n\t},\n\tSCOOTER_SEATED: {\n\t\tsvg: 'seated_scooter',\n\t\tstation: 'seated_scooter_station',\n\t\tvehicle: 'floating_seated_scooter',\n\t\tcluster: 'floating_seated_scooter_cluster',\n\t\tlabel: t.scooterSeated\n\t},\n\tSCOOTER_STANDING: {\n\t\tsvg: 'scooter',\n\t\tstation: 'scooter_station',\n\t\tvehicle: 'floating_scooter',\n\t\tcluster: 'floating_scooter_cluster',\n\t\tlabel: t.scooterStanding\n\t},\n\tOTHER: {\n\t\tsvg: 'other',\n\t\tstation: 'other_station',\n\t\tvehicle: 'floating_other',\n\t\tcluster: 'floating_other_cluster',\n\t\tlabel: t.unknownVehicleType\n\t}\n};\n\nexport const propulsionTypes: Record<\n\tRentalPropulsionType,\n\t{ component: typeof LucideIcon; title: string } | null\n> = {\n\tELECTRIC: { component: Zap, title: t.electric },\n\tELECTRIC_ASSIST: { component: Zap, title: t.electricAssist },\n\tHYBRID: { component: PlugZap, title: t.hybrid },\n\tPLUG_IN_HYBRID: { component: PlugZap, title: t.plugInHybrid },\n\tCOMBUSTION: { component: Fuel, title: t.combustion },\n\tCOMBUSTION_DIESEL: { component: Fuel, title: t.combustionDiesel },\n\tHYDROGEN_FUEL_CELL: { component: Fuel, title: t.hydrogenFuelCell },\n\tHUMAN: null\n};\n\nexport const returnConstraints: Record<\n\tRentalReturnConstraint,\n\t{ component: typeof LucideIcon; title: string } | null\n> = {\n\tANY_STATION: { component: FlagTriangleLeft, title: t.returnOnlyAtStations },\n\tROUNDTRIP_STATION: { component: RefreshCcw, title: t.roundtripStationReturnConstraint },\n\tNONE: null\n};\n\nexport const getIconBaseName = (formFactor: RentalFormFactor, type: IconType) =>\n\tformFactorAssets[formFactor][type];\n\nexport const getIconUrl = (formFactor: RentalFormFactor, type: IconType) =>\n\t`${ICON_BASE_PATH}${getIconBaseName(formFactor, type)}.svg`;\n\nconst iconTypeDimensions: Record<IconType, IconDimensions> = {\n\tstation: { width: 43, height: 44 },\n\tvehicle: { width: 27, height: 27 },\n\tcluster: { width: 35, height: 36 }\n};\n\nexport const getIconDimensions = (type: IconType): IconDimensions => iconTypeDimensions[type];\n\nexport async function colorizeIcon(\n\tsvgUrl: string,\n\tcolor: string,\n\tdimensions: IconDimensions\n): Promise<MapLibreImageSource> {\n\tif (!browser) {\n\t\tthrow new Error('colorizeIcon is not supported in this environment');\n\t}\n\n\tconst response = await fetch(svgUrl);\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to load icon: ${response.status} ${response.statusText}`);\n\t}\n\n\tconst svgContent = await response.text();\n\tconst parser = new DOMParser();\n\tconst svgDoc = parser.parseFromString(svgContent, 'image/svg+xml');\n\n\tif (svgDoc.getElementsByTagName('parsererror').length > 0) {\n\t\tthrow new Error('Invalid SVG content');\n\t}\n\n\tconst rootElement = svgDoc.documentElement;\n\tif (!(rootElement instanceof SVGSVGElement)) {\n\t\tthrow new Error('Provided file is not an SVG');\n\t}\n\tconst svgRoot = rootElement;\n\n\tif (!svgRoot.getAttribute('xmlns')) {\n\t\tsvgRoot.setAttribute('xmlns', 'http://www.w3.org/2000/svg');\n\t}\n\n\tconst existingStyle = svgRoot.getAttribute('style');\n\tconst colorStyle = `color: ${color}`;\n\tconst mergedStyle = existingStyle ? `${existingStyle};${colorStyle}` : colorStyle;\n\tsvgRoot.setAttribute('style', mergedStyle);\n\tsvgRoot.setAttribute('color', color);\n\tsvgRoot.setAttribute('width', `${dimensions.width}`);\n\tsvgRoot.setAttribute('height', `${dimensions.height}`);\n\n\tconst serializer = new XMLSerializer();\n\tconst serialized = serializer.serializeToString(svgDoc);\n\tconst blob = new Blob([serialized], { type: 'image/svg+xml;charset=utf-8' });\n\n\tif (typeof createImageBitmap === 'function') {\n\t\ttry {\n\t\t\tconst bitmap = await createImageBitmap(blob);\n\t\t\treturn bitmap;\n\t\t} catch (_error) {} // eslint-disable-line\n\t}\n\n\treturn await new Promise<MapLibreImageSource>((resolve, reject) => {\n\t\tconst blob_url = URL.createObjectURL(blob);\n\t\tconst image = new Image();\n\t\timage.crossOrigin = 'anonymous';\n\t\timage.decoding = 'async';\n\t\timage.onload = () => {\n\t\t\tURL.revokeObjectURL(blob_url);\n\t\t\tif (dimensions) {\n\t\t\t\timage.width = dimensions.width;\n\t\t\t\timage.height = dimensions.height;\n\t\t\t}\n\t\t\tresolve(image);\n\t\t};\n\t\timage.onerror = () => {\n\t\t\tURL.revokeObjectURL(blob_url);\n\t\t\treject(new Error('Failed to load generated image'));\n\t\t};\n\t\timage.src = blob_url;\n\t});\n}\n"
  },
  {
    "path": "ui/src/lib/map/rentals/style.ts",
    "content": "import type { ExpressionSpecification } from 'maplibre-gl';\n\nexport const createZoomScaledSize = (baseSize: number): ExpressionSpecification => [\n\t'interpolate',\n\t['linear'],\n\t['zoom'],\n\t14,\n\tbaseSize * 0.6,\n\t18,\n\tbaseSize\n];\n\nexport const zoomScaledIconSize = createZoomScaledSize(1);\nexport const zoomScaledTextSizeMedium = createZoomScaledSize(12);\nexport const zoomScaledTextSizeSmall = createZoomScaledSize(10);\n\nexport const createZoomScaledTextOffset = (\n\tbaseOffset: [number, number]\n): ExpressionSpecification => [\n\t'interpolate',\n\t['linear'],\n\t['zoom'],\n\t14,\n\t['literal', [baseOffset[0] * 0.8, baseOffset[1] * 0.9]],\n\t18,\n\t['literal', baseOffset]\n];\n\nexport const zoomScaledTextOffset = createZoomScaledTextOffset([0.8, -1.25]);\n\nexport const DEFAULT_COLOR = '#2563eb';\nexport const DEFAULT_CONTRAST_COLOR = '#ffffff';\n"
  },
  {
    "path": "ui/src/lib/map/rentals/zone-fill-layer.ts",
    "content": "import earcut from 'earcut';\nimport { flatten } from 'earcut';\nimport maplibregl from 'maplibre-gl';\nimport { type CustomRenderMethodInput, type Map as MapLibreMap, type PointLike } from 'maplibre-gl';\nimport type { Position } from 'geojson';\n\nimport type { RentalZoneFeature, RentalZoneFeatureProperties } from './zone-types';\n\ntype GLContext = WebGLRenderingContext | WebGL2RenderingContext;\n\nconst DEFAULT_OPACITY = 0.4;\nconst STRIPE_WIDTH_PX = 6.0;\nconst STRIPE_OPACITY_VARIATION = 0.1;\nconst POSITION_COMPONENTS = 2;\nconst POSITION_STRIDE_BYTES = POSITION_COMPONENTS * Float32Array.BYTES_PER_ELEMENT;\nconst COLOR_COMPONENTS = 4;\nconst COLOR_STRIDE_BYTES = COLOR_COMPONENTS * Float32Array.BYTES_PER_ELEMENT;\n\nconst FILL_VERTEX_SHADER_SOURCE = `#version 300 es\nprecision highp float;\nin vec2 a_pos;\nin vec4 a_color;\nout vec4 v_color;\nuniform vec4 u_zone_base;\nuniform vec4 u_zone_scale_x;\nuniform vec4 u_zone_scale_y;\nvoid main() {\n\tv_color = a_color;\n\tgl_Position = u_zone_base + a_pos.x * u_zone_scale_x + a_pos.y * u_zone_scale_y;\n}\n`;\n\nconst FILL_FRAGMENT_SHADER_SOURCE = `#version 300 es\nprecision mediump float;\nin vec4 v_color;\nout vec4 fragColor;\nvoid main() {\n\tfragColor = v_color;\n}\n`;\n\ntype ZoneGeometry = {\n\tvertices: number[]; // mercator [x0, y0, x1, y1, ...]\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n};\n\ntype FillProgramState = {\n\tprogram: WebGLProgram;\n\tpositionLocation: number;\n\tcolorLocation: number;\n\tbaseLocation: WebGLUniformLocation | null;\n\tscaleXLocation: WebGLUniformLocation | null;\n\tscaleYLocation: WebGLUniformLocation | null;\n};\n\ntype ZoneFillLayerOptions = {\n\tid: string;\n\topacity?: number;\n};\n\nconst QUAD_VERTICES = new Float32Array([-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1]);\n\nconst SCREEN_VERTEX_SHADER_SOURCE = `\nattribute vec2 a_pos;\nattribute vec2 a_tex_coord;\nvarying vec2 v_tex_coord;\nvoid main() {\n\tv_tex_coord = a_tex_coord;\n\tgl_Position = vec4(a_pos, 0.0, 1.0);\n}\n`;\n\nconst SCREEN_FRAGMENT_SHADER_SOURCE = `\nprecision highp float;\nuniform sampler2D u_texture;\nuniform float u_opacity_primary;\nuniform float u_opacity_secondary;\nuniform float u_stripe_width;\nvarying vec2 v_tex_coord;\nvoid main() {\n\tvec4 color = texture2D(u_texture, v_tex_coord);\n\tif (color.a == 0.0) {\n\t\tdiscard;\n\t}\n\tfloat diagonal = gl_FragCoord.x + gl_FragCoord.y;\n\tfloat stripeIndex = mod(floor(diagonal / u_stripe_width), 2.0);\n\tfloat opacity = mix(u_opacity_primary, u_opacity_secondary, stripeIndex);\n\tgl_FragColor = vec4(color.rgb, color.a * opacity);\n}\n`;\n\nconst ZONE_COLOR_ALLOWED = new Float32Array([0.13333333, 0.77254902, 0.36862745, 1]); // #22c55e (green)\nconst ZONE_COLOR_FORBIDDEN = new Float32Array([0.9372549, 0.26666667, 0.26666667, 1]); // #ef4444 (red)\nconst ZONE_COLOR_RESTRICTED = new Float32Array([1, 0.84313725, 0, 1]); // #ffd700 (yellow)\nconst ZONE_COLOR_STATION = new Float32Array([0.25882354, 0.52156866, 0.95686275, 1]); // #4287f5 (blue)\n\nexport const getZoneColor = (properties: RentalZoneFeatureProperties) => {\n\tif (properties.stationArea) {\n\t\treturn ZONE_COLOR_STATION;\n\t}\n\tif (properties.rideEndAllowed) {\n\t\treturn ZONE_COLOR_ALLOWED;\n\t}\n\tif (!properties.rideThroughAllowed) {\n\t\treturn ZONE_COLOR_FORBIDDEN;\n\t}\n\treturn ZONE_COLOR_RESTRICTED;\n};\n\nconst encodePickingColor = (i: number): Float32Array => {\n\treturn new Float32Array([\n\t\t((i >> 16) & 0xff) / 0xff,\n\t\t((i >> 8) & 0xff) / 0xff,\n\t\t(i & 0xff) / 0xff,\n\t\t1\n\t]);\n};\n\nconst decodePickingColor = (px: Uint8Array): number => {\n\treturn (px[0] << 16) | (px[1] << 8) | px[2];\n};\n\ntype ZoneClipFrame = {\n\tbase: Float32Array;\n\tscaleX: Float32Array;\n\tscaleY: Float32Array;\n};\n\nconst toClipCoordinates = (\n\tmap: MapLibreMap,\n\twidth: number,\n\theight: number,\n\tpixelRatioX: number,\n\tpixelRatioY: number,\n\tlngLat: maplibregl.LngLat\n): Float32Array => {\n\tconst point = map.project(lngLat);\n\tconst px = point.x * pixelRatioX;\n\tconst py = point.y * pixelRatioY;\n\tconst clipX = (px / width) * 2 - 1;\n\tconst clipY = 1 - (py / height) * 2;\n\treturn new Float32Array([clipX, clipY, 0, 1]);\n};\n\nconst computeZoneClipFrame = (\n\tmap: MapLibreMap,\n\twidth: number,\n\theight: number,\n\tpixelRatioX: number,\n\tpixelRatioY: number,\n\torigin: Float32Array,\n\textent: Float32Array\n): ZoneClipFrame => {\n\tconst originLngLat = new maplibregl.MercatorCoordinate(origin[0], origin[1]).toLngLat();\n\tconst maxLngLat = new maplibregl.MercatorCoordinate(\n\t\torigin[0] + extent[0],\n\t\torigin[1] + extent[1]\n\t).toLngLat();\n\n\tconst base = toClipCoordinates(map, width, height, pixelRatioX, pixelRatioY, originLngLat);\n\tconst maxClip = toClipCoordinates(map, width, height, pixelRatioX, pixelRatioY, maxLngLat);\n\n\tconst scaleX = new Float32Array([maxClip[0] - base[0], 0, 0, 0]);\n\tconst scaleY = new Float32Array([0, maxClip[1] - base[1], 0, 0]);\n\n\treturn { base, scaleX, scaleY };\n};\n\nconst toPoint = (p: PointLike): maplibregl.Point => {\n\treturn Array.isArray(p) ? new maplibregl.Point(p[0], p[1]) : p;\n};\n\nconst WEB_MERCATOR_MAX_LATITUDE = 85.051129;\nconst clampLngLatToWebMercator = (lng: number, lat: number): [number, number] => {\n\treturn [\n\t\tMath.min(180, Math.max(-180, lng)),\n\t\tMath.min(WEB_MERCATOR_MAX_LATITUDE, Math.max(-WEB_MERCATOR_MAX_LATITUDE, lat))\n\t];\n};\n\nexport class ZoneFillLayer implements maplibregl.CustomLayerInterface {\n\tid: string;\n\ttype: 'custom' = 'custom' as const;\n\trenderingMode: '2d' = '2d' as const;\n\n\tprivate opacity: number;\n\tprivate gl: GLContext | null = null;\n\tprivate map: MapLibreMap | null = null;\n\tprivate screenProgram: WebGLProgram | null = null;\n\tprivate framebuffer: WebGLFramebuffer | null = null;\n\tprivate texture: WebGLTexture | null = null;\n\tprivate pickingFramebuffer: WebGLFramebuffer | null = null;\n\tprivate pickingTexture: WebGLTexture | null = null;\n\tprivate quadBuffer: WebGLBuffer | null = null;\n\tprivate features: RentalZoneFeature[] = [];\n\tprivate geometryDirty = true;\n\tprivate width = 0;\n\tprivate height = 0;\n\tprivate pickingWidth = 0;\n\tprivate pickingHeight = 0;\n\tprivate fillProgram: FillProgramState | null = null;\n\tprivate pickingLookup = new Map<number, RentalZoneFeature>();\n\tprivate pickingPixel = new Uint8Array(4);\n\tprivate positionBuffer: WebGLBuffer | null = null;\n\tprivate colorBuffer: WebGLBuffer | null = null;\n\tprivate pickingColorBuffer: WebGLBuffer | null = null;\n\tprivate vertexCount = 0;\n\tprivate globalOrigin = new Float32Array([0, 0]);\n\tprivate globalExtent = new Float32Array([1, 1]);\n\n\tprivate screenPositionLocation = -1;\n\tprivate screenTexCoordLocation = -1;\n\tprivate screenTextureLocation: WebGLUniformLocation | null = null;\n\tprivate screenOpacityPrimaryLocation: WebGLUniformLocation | null = null;\n\tprivate screenOpacitySecondaryLocation: WebGLUniformLocation | null = null;\n\tprivate screenStripeWidthLocation: WebGLUniformLocation | null = null;\n\n\tconstructor(options: ZoneFillLayerOptions) {\n\t\tthis.id = options.id;\n\t\tthis.opacity = options.opacity ?? DEFAULT_OPACITY;\n\t}\n\n\tsetOpacity(opacity: number) {\n\t\tif (this.opacity === opacity) {\n\t\t\treturn;\n\t\t}\n\t\tthis.opacity = opacity;\n\t\tthis.map?.triggerRepaint();\n\t}\n\n\tsetFeatures(features: RentalZoneFeature[]) {\n\t\tthis.features = features;\n\t\tthis.geometryDirty = true;\n\t\tthis.updateGeometry();\n\t\tthis.map?.triggerRepaint();\n\t}\n\n\tonAdd(map: MapLibreMap, gl: GLContext) {\n\t\tthis.map = map;\n\t\tthis.gl = gl;\n\t\tthis.initialize(gl);\n\t\tthis.updateGeometry();\n\t}\n\n\tonRemove(_map: MapLibreMap, gl: GLContext) {\n\t\tthis.cleanup(gl);\n\t}\n\n\tcleanup(gl?: GLContext) {\n\t\tif (!gl && this.gl) {\n\t\t\tgl = this.gl;\n\t\t}\n\t\tif (!gl) {\n\t\t\treturn;\n\t\t}\n\t\tthis.deleteGeometryBuffers(gl);\n\t\tif (this.framebuffer) {\n\t\t\tgl.deleteFramebuffer(this.framebuffer);\n\t\t\tthis.framebuffer = null;\n\t\t}\n\t\tif (this.texture) {\n\t\t\tgl.deleteTexture(this.texture);\n\t\t\tthis.texture = null;\n\t\t}\n\t\tif (this.pickingFramebuffer) {\n\t\t\tgl.deleteFramebuffer(this.pickingFramebuffer);\n\t\t\tthis.pickingFramebuffer = null;\n\t\t}\n\t\tif (this.pickingTexture) {\n\t\t\tgl.deleteTexture(this.pickingTexture);\n\t\t\tthis.pickingTexture = null;\n\t\t}\n\t\tif (this.quadBuffer) {\n\t\t\tgl.deleteBuffer(this.quadBuffer);\n\t\t\tthis.quadBuffer = null;\n\t\t}\n\t\tif (this.fillProgram) {\n\t\t\tgl.deleteProgram(this.fillProgram.program);\n\t\t\tthis.fillProgram = null;\n\t\t}\n\t\tthis.pickingLookup.clear();\n\t\tif (this.screenProgram) {\n\t\t\tgl.deleteProgram(this.screenProgram);\n\t\t\tthis.screenProgram = null;\n\t\t}\n\t\tthis.geometryDirty = true;\n\t\tthis.width = 0;\n\t\tthis.height = 0;\n\t\tthis.pickingWidth = 0;\n\t\tthis.pickingHeight = 0;\n\t\tthis.gl = null;\n\t\tthis.map = null;\n\t}\n\n\tprerender(gl: GLContext, _options: CustomRenderMethodInput) {\n\t\tif (\n\t\t\t!this.map ||\n\t\t\tthis.vertexCount === 0 ||\n\t\t\t!this.positionBuffer ||\n\t\t\t!this.colorBuffer ||\n\t\t\t!this.pickingColorBuffer\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst map = this.map;\n\t\tconst width = gl.drawingBufferWidth;\n\t\tconst height = gl.drawingBufferHeight;\n\t\tif (width === 0 || height === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst fillProgram = this.ensureFillProgram(gl);\n\t\tif (!fillProgram || fillProgram.positionLocation < 0 || fillProgram.colorLocation < 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst canvas = map.getCanvas();\n\t\tconst rect = canvas.getBoundingClientRect();\n\t\tconst cssWidth = rect.width;\n\t\tconst cssHeight = rect.height;\n\t\tif (cssWidth === 0 || cssHeight === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst pixelRatioX = width / cssWidth;\n\t\tconst pixelRatioY = height / cssHeight;\n\t\tconst clipFrame = computeZoneClipFrame(\n\t\t\tmap,\n\t\t\twidth,\n\t\t\theight,\n\t\t\tpixelRatioX,\n\t\t\tpixelRatioY,\n\t\t\tthis.globalOrigin,\n\t\t\tthis.globalExtent\n\t\t);\n\n\t\tthis.ensureFramebuffer(gl, width, height);\n\t\tthis.ensurePickingFramebuffer(gl, width, height);\n\t\tif (!this.framebuffer || !this.texture || !this.pickingFramebuffer || !this.pickingTexture) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;\n\t\tconst previousViewport = gl.getParameter(gl.VIEWPORT) as Int32Array;\n\t\tconst blendEnabled = gl.isEnabled(gl.BLEND);\n\t\tconst depthTestEnabled = gl.isEnabled(gl.DEPTH_TEST);\n\t\tconst stencilTestEnabled = gl.isEnabled(gl.STENCIL_TEST);\n\t\tconst cullFaceEnabled = gl.isEnabled(gl.CULL_FACE);\n\n\t\tgl.disable(gl.BLEND);\n\t\tgl.disable(gl.DEPTH_TEST);\n\t\tgl.disable(gl.STENCIL_TEST);\n\t\tgl.disable(gl.CULL_FACE);\n\n\t\tconst renderToFramebuffer = (fb: WebGLFramebuffer, colorBuffer: WebGLBuffer) => {\n\t\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, fb);\n\t\t\tgl.viewport(0, 0, width, height);\n\t\t\tgl.clearColor(0, 0, 0, 0);\n\t\t\tgl.clear(gl.COLOR_BUFFER_BIT);\n\n\t\t\tgl.useProgram(fillProgram.program);\n\t\t\tgl.uniform4fv(fillProgram.baseLocation, clipFrame.base);\n\t\t\tgl.uniform4fv(fillProgram.scaleXLocation, clipFrame.scaleX);\n\t\t\tgl.uniform4fv(fillProgram.scaleYLocation, clipFrame.scaleY);\n\n\t\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);\n\t\t\tgl.enableVertexAttribArray(fillProgram.positionLocation);\n\t\t\tgl.vertexAttribPointer(\n\t\t\t\tfillProgram.positionLocation,\n\t\t\t\tPOSITION_COMPONENTS,\n\t\t\t\tgl.FLOAT,\n\t\t\t\tfalse,\n\t\t\t\tPOSITION_STRIDE_BYTES,\n\t\t\t\t0\n\t\t\t);\n\n\t\t\tgl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);\n\t\t\tgl.enableVertexAttribArray(fillProgram.colorLocation);\n\t\t\tgl.vertexAttribPointer(\n\t\t\t\tfillProgram.colorLocation,\n\t\t\t\tCOLOR_COMPONENTS,\n\t\t\t\tgl.FLOAT,\n\t\t\t\tfalse,\n\t\t\t\tCOLOR_STRIDE_BYTES,\n\t\t\t\t0\n\t\t\t);\n\n\t\t\tgl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);\n\n\t\t\tgl.disableVertexAttribArray(fillProgram.positionLocation);\n\t\t\tgl.disableVertexAttribArray(fillProgram.colorLocation);\n\t\t\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\n\t\t};\n\n\t\trenderToFramebuffer(this.framebuffer, this.colorBuffer);\n\t\trenderToFramebuffer(this.pickingFramebuffer, this.pickingColorBuffer);\n\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);\n\t\tgl.viewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]);\n\n\t\tif (blendEnabled) {\n\t\t\tgl.enable(gl.BLEND);\n\t\t}\n\t\tif (depthTestEnabled) {\n\t\t\tgl.enable(gl.DEPTH_TEST);\n\t\t}\n\t\tif (stencilTestEnabled) {\n\t\t\tgl.enable(gl.STENCIL_TEST);\n\t\t}\n\t\tif (cullFaceEnabled) {\n\t\t\tgl.enable(gl.CULL_FACE);\n\t\t}\n\t}\n\n\trender(gl: GLContext, _options: CustomRenderMethodInput) {\n\t\tif (!this.screenProgram || !this.texture || !this.quadBuffer || this.vertexCount === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tgl.useProgram(this.screenProgram);\n\n\t\tconst prevBlendSrcRGB = gl.getParameter(gl.BLEND_SRC_RGB) as number;\n\t\tconst prevBlendDstRGB = gl.getParameter(gl.BLEND_DST_RGB) as number;\n\t\tconst prevBlendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA) as number;\n\t\tconst prevBlendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA) as number;\n\t\tconst prevBlendEquationRGB = gl.getParameter(gl.BLEND_EQUATION_RGB) as number;\n\t\tconst prevBlendEquationAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA) as number;\n\n\t\tgl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n\t\tgl.enableVertexAttribArray(this.screenPositionLocation);\n\t\tgl.enableVertexAttribArray(this.screenTexCoordLocation);\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);\n\t\tgl.vertexAttribPointer(this.screenPositionLocation, 2, gl.FLOAT, false, 16, 0);\n\t\tgl.vertexAttribPointer(this.screenTexCoordLocation, 2, gl.FLOAT, false, 16, 8);\n\n\t\tgl.activeTexture(gl.TEXTURE0);\n\t\tgl.bindTexture(gl.TEXTURE_2D, this.texture);\n\t\tgl.uniform1i(this.screenTextureLocation, 0);\n\t\tconst minOpacity = Math.max(this.opacity - STRIPE_OPACITY_VARIATION, 0.0);\n\t\tconst maxOpacity = Math.min(this.opacity + STRIPE_OPACITY_VARIATION, 1.0);\n\t\tgl.uniform1f(this.screenOpacityPrimaryLocation, minOpacity);\n\t\tgl.uniform1f(this.screenOpacitySecondaryLocation, maxOpacity);\n\t\tgl.uniform1f(this.screenStripeWidthLocation, STRIPE_WIDTH_PX);\n\n\t\tgl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n\t\tgl.bindTexture(gl.TEXTURE_2D, null);\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\n\t\tgl.disableVertexAttribArray(this.screenPositionLocation);\n\t\tgl.disableVertexAttribArray(this.screenTexCoordLocation);\n\n\t\tgl.blendFuncSeparate(prevBlendSrcRGB, prevBlendDstRGB, prevBlendSrcAlpha, prevBlendDstAlpha);\n\t\tgl.blendEquationSeparate(prevBlendEquationRGB, prevBlendEquationAlpha);\n\t}\n\n\tprivate initialize(gl: GLContext) {\n\t\tthis.screenProgram = this.createProgram(\n\t\t\tgl,\n\t\t\tSCREEN_VERTEX_SHADER_SOURCE,\n\t\t\tSCREEN_FRAGMENT_SHADER_SOURCE\n\t\t);\n\n\t\tif (!this.screenProgram) {\n\t\t\tthrow new Error('Failed to initialize zone fill shaders');\n\t\t}\n\n\t\tthis.screenPositionLocation = gl.getAttribLocation(this.screenProgram, 'a_pos');\n\t\tthis.screenTexCoordLocation = gl.getAttribLocation(this.screenProgram, 'a_tex_coord');\n\t\tthis.screenTextureLocation = gl.getUniformLocation(this.screenProgram, 'u_texture');\n\t\tthis.screenOpacityPrimaryLocation = gl.getUniformLocation(\n\t\t\tthis.screenProgram,\n\t\t\t'u_opacity_primary'\n\t\t);\n\t\tthis.screenOpacitySecondaryLocation = gl.getUniformLocation(\n\t\t\tthis.screenProgram,\n\t\t\t'u_opacity_secondary'\n\t\t);\n\t\tthis.screenStripeWidthLocation = gl.getUniformLocation(this.screenProgram, 'u_stripe_width');\n\n\t\tthis.quadBuffer = gl.createBuffer();\n\t\tif (!this.quadBuffer) {\n\t\t\tthrow new Error('Failed to allocate quad buffer');\n\t\t}\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);\n\t\tgl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTICES, gl.STATIC_DRAW);\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\n\t}\n\n\tprivate ensureFramebuffer(gl: GLContext, width: number, height: number) {\n\t\tif (this.framebuffer && this.texture && this.width === width && this.height === height) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.width = width;\n\t\tthis.height = height;\n\n\t\tif (!this.framebuffer) {\n\t\t\tthis.framebuffer = gl.createFramebuffer();\n\t\t}\n\t\tif (!this.texture) {\n\t\t\tthis.texture = gl.createTexture();\n\t\t}\n\t\tif (!this.framebuffer || !this.texture) {\n\t\t\treturn;\n\t\t}\n\n\t\tgl.bindTexture(gl.TEXTURE_2D, this.texture);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\t\tgl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);\n\t\tgl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);\n\n\t\tconst status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n\t\tif (status !== gl.FRAMEBUFFER_COMPLETE) {\n\t\t\tconsole.error('[ZoneFillLayer] Incomplete framebuffer:', status);\n\t\t}\n\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\t\tgl.bindTexture(gl.TEXTURE_2D, null);\n\t}\n\n\tprivate ensurePickingFramebuffer(gl: GLContext, width: number, height: number) {\n\t\tif (\n\t\t\tthis.pickingFramebuffer &&\n\t\t\tthis.pickingTexture &&\n\t\t\tthis.pickingWidth === width &&\n\t\t\tthis.pickingHeight === height\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.pickingFramebuffer) {\n\t\t\tthis.pickingFramebuffer = gl.createFramebuffer();\n\t\t}\n\t\tif (!this.pickingTexture) {\n\t\t\tthis.pickingTexture = gl.createTexture();\n\t\t}\n\t\tif (!this.pickingFramebuffer || !this.pickingTexture) {\n\t\t\treturn;\n\t\t}\n\n\t\tgl.bindTexture(gl.TEXTURE_2D, this.pickingTexture);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\t\tgl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, this.pickingFramebuffer);\n\t\tgl.framebufferTexture2D(\n\t\t\tgl.FRAMEBUFFER,\n\t\t\tgl.COLOR_ATTACHMENT0,\n\t\t\tgl.TEXTURE_2D,\n\t\t\tthis.pickingTexture,\n\t\t\t0\n\t\t);\n\n\t\tconst status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);\n\t\tif (status !== gl.FRAMEBUFFER_COMPLETE) {\n\t\t\tconsole.error('[ZoneFillLayer] Incomplete picking framebuffer:', status);\n\t\t}\n\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\t\tgl.bindTexture(gl.TEXTURE_2D, null);\n\n\t\tthis.pickingWidth = width;\n\t\tthis.pickingHeight = height;\n\t}\n\n\tprivate ensureFillProgram(gl: GLContext): FillProgramState | null {\n\t\tif (this.fillProgram) {\n\t\t\treturn this.fillProgram;\n\t\t}\n\n\t\tconst program = this.createProgram(gl, FILL_VERTEX_SHADER_SOURCE, FILL_FRAGMENT_SHADER_SOURCE);\n\t\tif (!program) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst state: FillProgramState = {\n\t\t\tprogram,\n\t\t\tpositionLocation: gl.getAttribLocation(program, 'a_pos'),\n\t\t\tcolorLocation: gl.getAttribLocation(program, 'a_color'),\n\t\t\tbaseLocation: gl.getUniformLocation(program, 'u_zone_base'),\n\t\t\tscaleXLocation: gl.getUniformLocation(program, 'u_zone_scale_x'),\n\t\t\tscaleYLocation: gl.getUniformLocation(program, 'u_zone_scale_y')\n\t\t};\n\n\t\tthis.fillProgram = state;\n\t\treturn state;\n\t}\n\n\tprivate createProgram(\n\t\tgl: GLContext,\n\t\tvertexSrc: string,\n\t\tfragmentSrc: string\n\t): WebGLProgram | null {\n\t\tconst vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, vertexSrc);\n\t\tconst fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);\n\t\tif (!vertexShader || !fragmentShader) {\n\t\t\treturn null;\n\t\t}\n\t\tconst program = gl.createProgram();\n\t\tif (!program) {\n\t\t\treturn null;\n\t\t}\n\t\tgl.attachShader(program, vertexShader);\n\t\tgl.attachShader(program, fragmentShader);\n\t\tgl.linkProgram(program);\n\t\tif (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n\t\t\tconsole.error('Failed to link shader program', gl.getProgramInfoLog(program));\n\t\t\tgl.deleteProgram(program);\n\t\t\treturn null;\n\t\t}\n\t\tgl.deleteShader(vertexShader);\n\t\tgl.deleteShader(fragmentShader);\n\t\treturn program;\n\t}\n\n\tprivate compileShader(gl: GLContext, type: number, source: string): WebGLShader | null {\n\t\tconst shader = gl.createShader(type);\n\t\tif (!shader) {\n\t\t\treturn null;\n\t\t}\n\t\tgl.shaderSource(shader, source);\n\t\tgl.compileShader(shader);\n\t\tif (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n\t\t\tconsole.error('Failed to compile shader', gl.getShaderInfoLog(shader));\n\t\t\tgl.deleteShader(shader);\n\t\t\treturn null;\n\t\t}\n\t\treturn shader;\n\t}\n\n\tprivate updateGeometry() {\n\t\tconst gl = this.gl;\n\t\tif (!gl || !this.geometryDirty) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.deleteGeometryBuffers(gl);\n\t\tthis.pickingLookup.clear();\n\n\t\tconst features = [...this.features].sort((a, b) => a.properties.z - b.properties.z);\n\t\tconst zoneGeometries: ZoneGeometry[] = [];\n\t\tconst colorValues: number[] = [];\n\t\tconst pickingValues: number[] = [];\n\t\tlet totalVertices = 0;\n\t\tlet globalMinX = Number.POSITIVE_INFINITY;\n\t\tlet globalMinY = Number.POSITIVE_INFINITY;\n\t\tlet globalMaxX = Number.NEGATIVE_INFINITY;\n\t\tlet globalMaxY = Number.NEGATIVE_INFINITY;\n\t\tlet zoneIdx = 0;\n\n\t\tfor (const feature of features) {\n\t\t\tconst geometry = this.buildZoneGeometry(feature);\n\t\t\tif (!geometry) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst vertexCount = geometry.vertices.length / POSITION_COMPONENTS;\n\t\t\tif (vertexCount === 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tzoneGeometries.push(geometry);\n\t\t\ttotalVertices += vertexCount;\n\t\t\tglobalMinX = Math.min(globalMinX, geometry.minX);\n\t\t\tglobalMinY = Math.min(globalMinY, geometry.minY);\n\t\t\tglobalMaxX = Math.max(globalMaxX, geometry.maxX);\n\t\t\tglobalMaxY = Math.max(globalMaxY, geometry.maxY);\n\n\t\t\tconst pickingIdx = zoneIdx + 1;\n\t\t\tzoneIdx += 1;\n\t\t\tconst color = getZoneColor(feature.properties);\n\t\t\tconst pickingColor = encodePickingColor(pickingIdx);\n\t\t\tthis.pickingLookup.set(pickingIdx, feature);\n\n\t\t\tfor (let i = 0; i < vertexCount; ++i) {\n\t\t\t\tcolorValues.push(color[0], color[1], color[2], color[3]);\n\t\t\t\tpickingValues.push(pickingColor[0], pickingColor[1], pickingColor[2], pickingColor[3]);\n\t\t\t}\n\t\t}\n\n\t\tconst extentX = globalMaxX - globalMinX;\n\t\tconst extentY = globalMaxY - globalMinY;\n\n\t\tif (\n\t\t\ttotalVertices === 0 ||\n\t\t\tglobalMinX === Number.POSITIVE_INFINITY ||\n\t\t\tglobalMinY === Number.POSITIVE_INFINITY ||\n\t\t\tglobalMaxX === Number.NEGATIVE_INFINITY ||\n\t\t\tglobalMaxY === Number.NEGATIVE_INFINITY ||\n\t\t\textentX === 0 ||\n\t\t\textentY === 0\n\t\t) {\n\t\t\tthis.vertexCount = 0;\n\t\t\tthis.geometryDirty = false;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.globalOrigin = new Float32Array([globalMinX, globalMinY]);\n\t\tthis.globalExtent = new Float32Array([extentX, extentY]);\n\n\t\tconst positions = new Float32Array(totalVertices * POSITION_COMPONENTS);\n\t\tlet posOffset = 0;\n\t\tfor (const geometry of zoneGeometries) {\n\t\t\tfor (let i = 0; i < geometry.vertices.length; i += POSITION_COMPONENTS) {\n\t\t\t\tconst x = geometry.vertices[i];\n\t\t\t\tconst y = geometry.vertices[i + 1];\n\t\t\t\tpositions[posOffset++] = (x - globalMinX) / extentX;\n\t\t\t\tpositions[posOffset++] = (y - globalMinY) / extentY;\n\t\t\t}\n\t\t}\n\n\t\tconst colorArray = new Float32Array(colorValues);\n\t\tconst pickingArray = new Float32Array(pickingValues);\n\n\t\tthis.positionBuffer = gl.createBuffer();\n\t\tthis.colorBuffer = gl.createBuffer();\n\t\tthis.pickingColorBuffer = gl.createBuffer();\n\t\tif (!this.positionBuffer || !this.colorBuffer || !this.pickingColorBuffer) {\n\t\t\tconsole.error('[ZoneFillLayer] Failed to allocate geometry buffers');\n\t\t\tthis.deleteGeometryBuffers(gl);\n\t\t\tthis.geometryDirty = false;\n\t\t\treturn;\n\t\t}\n\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);\n\t\tgl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);\n\t\tgl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW);\n\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, this.pickingColorBuffer);\n\t\tgl.bufferData(gl.ARRAY_BUFFER, pickingArray, gl.STATIC_DRAW);\n\n\t\tgl.bindBuffer(gl.ARRAY_BUFFER, null);\n\n\t\tthis.vertexCount = totalVertices;\n\t\tthis.geometryDirty = false;\n\t}\n\n\tprivate deleteGeometryBuffers(gl: GLContext) {\n\t\tif (this.positionBuffer) {\n\t\t\tgl.deleteBuffer(this.positionBuffer);\n\t\t\tthis.positionBuffer = null;\n\t\t}\n\t\tif (this.colorBuffer) {\n\t\t\tgl.deleteBuffer(this.colorBuffer);\n\t\t\tthis.colorBuffer = null;\n\t\t}\n\t\tif (this.pickingColorBuffer) {\n\t\t\tgl.deleteBuffer(this.pickingColorBuffer);\n\t\t\tthis.pickingColorBuffer = null;\n\t\t}\n\t\tthis.vertexCount = 0;\n\t\tthis.globalOrigin = new Float32Array([0, 0]);\n\t\tthis.globalExtent = new Float32Array([1, 1]);\n\t}\n\n\tpickFeatureAt(pointLike: PointLike): RentalZoneFeature | null {\n\t\tif (!this.map || !this.gl || !this.pickingFramebuffer || !this.pickingTexture) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst pt = toPoint(pointLike);\n\t\tconst canvas = this.map.getCanvas();\n\t\tconst rect = canvas.getBoundingClientRect();\n\t\tif (rect.width === 0 || rect.height === 0) {\n\t\t\treturn null;\n\t\t}\n\t\tconst scaleX = canvas.width / rect.width;\n\t\tconst scaleY = canvas.height / rect.height;\n\t\tconst pixelX = Math.floor(pt.x * scaleX);\n\t\tconst pixelY = Math.floor(canvas.height - pt.y * scaleY - 1);\n\n\t\tconst previousFramebuffer = this.gl.getParameter(\n\t\t\tthis.gl.FRAMEBUFFER_BINDING\n\t\t) as WebGLFramebuffer | null;\n\t\tthis.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.pickingFramebuffer);\n\t\tthis.gl.readPixels(\n\t\t\tpixelX,\n\t\t\tpixelY,\n\t\t\t1,\n\t\t\t1,\n\t\t\tthis.gl.RGBA,\n\t\t\tthis.gl.UNSIGNED_BYTE,\n\t\t\tthis.pickingPixel\n\t\t);\n\t\tthis.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);\n\n\t\tconst index = decodePickingColor(this.pickingPixel);\n\t\tif (index === 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.pickingLookup.get(index) ?? null;\n\t}\n\n\tprivate buildZoneGeometry(feature: RentalZoneFeature): ZoneGeometry | null {\n\t\tconst vertices: number[] = [];\n\t\tlet minX = Number.POSITIVE_INFINITY;\n\t\tlet minY = Number.POSITIVE_INFINITY;\n\t\tlet maxX = Number.NEGATIVE_INFINITY;\n\t\tlet maxY = Number.NEGATIVE_INFINITY;\n\n\t\tconst appendPoly = (coords: Position[][]) => {\n\t\t\tif (!coords.length) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst mercatorCoords = coords.map((ring) =>\n\t\t\t\tring.map(([lng, lat]) => {\n\t\t\t\t\tconst clamped = clampLngLatToWebMercator(lng, lat);\n\t\t\t\t\tconst merc = maplibregl.MercatorCoordinate.fromLngLat(clamped);\n\t\t\t\t\tminX = Math.min(minX, merc.x);\n\t\t\t\t\tminY = Math.min(minY, merc.y);\n\t\t\t\t\tmaxX = Math.max(maxX, merc.x);\n\t\t\t\t\tmaxY = Math.max(maxY, merc.y);\n\t\t\t\t\treturn [merc.x, merc.y];\n\t\t\t\t})\n\t\t\t);\n\t\t\tconst data = flatten(mercatorCoords);\n\t\t\tconst indices = earcut(data.vertices, data.holes, data.dimensions);\n\t\t\tconst stride = data.dimensions;\n\n\t\t\tfor (const index of indices) {\n\t\t\t\tconst base = index * stride;\n\t\t\t\tvertices.push(data.vertices[base], data.vertices[base + 1]);\n\t\t\t}\n\t\t};\n\n\t\tfor (const polygon of feature.geometry.coordinates) {\n\t\t\tappendPoly(polygon);\n\t\t}\n\n\t\tif (vertices.length === 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { vertices, minX, minY, maxX, maxY };\n\t}\n}\n"
  },
  {
    "path": "ui/src/lib/map/rentals/zone-types.ts",
    "content": "import type { Feature, FeatureCollection, MultiPolygon } from 'geojson';\n\nexport type RentalZoneFeatureProperties = {\n\tzoneIndex?: number;\n\tstationIndex?: number;\n\tproviderId: string;\n\tz: number;\n\trideEndAllowed: boolean;\n\trideThroughAllowed: boolean;\n\tstationArea: boolean;\n};\n\nexport type RentalZoneFeature = Feature<MultiPolygon, RentalZoneFeatureProperties>;\n\nexport type RentalZoneFeatureCollection = FeatureCollection<\n\tMultiPolygon,\n\tRentalZoneFeatureProperties\n>;\n"
  },
  {
    "path": "ui/src/lib/map/routes/Routes.svelte",
    "content": "<script lang=\"ts\">\n\timport { SvelteMap, SvelteSet } from 'svelte/reactivity';\n\timport { ArrowLeft, ArrowRight, Download, LoaderCircle } from '@lucide/svelte';\n\timport { getModeName } from '$lib/getModeName';\n\timport { lngLatToStr } from '$lib/lngLatToStr';\n\timport Control from '$lib/map/Control.svelte';\n\timport GeoJSON from '$lib/map/GeoJSON.svelte';\n\timport Layer from '$lib/map/Layer.svelte';\n\timport Popup from '$lib/map/Popup.svelte';\n\timport polyline from '@mapbox/polyline';\n\timport { colord } from 'colord';\n\timport type { Position } from 'geojson';\n\timport maplibregl from 'maplibre-gl';\n\timport type { FeatureCollection, LineString, Point } from 'geojson';\n\timport {\n\t\trouteDetails,\n\t\troutes,\n\t\ttype Leg,\n\t\ttype Place,\n\t\ttype RouteInfo,\n\t\ttype RoutePolyline\n\t} from '@motis-project/motis-client';\n\timport { getDecorativeColors } from '$lib/map/colors';\n\timport { t } from '$lib/i18n/translation';\n\timport { client } from '@motis-project/motis-client';\n\n\tlet {\n\t\tmap,\n\t\tbounds,\n\t\tzoom,\n\t\tshapesDebugEnabled = false\n\t}: {\n\t\tmap: maplibregl.Map | undefined;\n\t\tbounds: maplibregl.LngLatBoundsLike | undefined;\n\t\tzoom: number;\n\t\tshapesDebugEnabled?: boolean;\n\t} = $props();\n\n\tconst FETCH_PADDING_RATIO = 0.5;\n\n\ttype RouteDetailsPayload = Awaited<ReturnType<typeof routeDetails>>['data'];\n\ttype RouteResponseData = {\n\t\troutes: RouteInfo[];\n\t\tpolylines: RoutePolyline[];\n\t\tstops: Place[];\n\t\tzoomFiltered: boolean;\n\t};\n\ttype RouteEntry = {\n\t\troute: RouteInfo;\n\t\tarrayIdx: number;\n\t\tcolor: string;\n\t\tdirectionDeg: number | null;\n\t};\n\ttype PopupSnapshot = {\n\t\tlngLat: maplibregl.LngLatLike;\n\t\tevent: maplibregl.MapMouseEvent;\n\t\tfeatures?: maplibregl.MapGeoJSONFeature[];\n\t};\n\ttype PopupController = {\n\t\topen?: (snapshot: PopupSnapshot) => void;\n\t\tclose?: () => void;\n\t\tgetSnapshot?: () => PopupSnapshot | null;\n\t\tonSnapshotChange?: (snapshot: PopupSnapshot | null) => void;\n\t};\n\ttype MapViewState = {\n\t\tcenter: [number, number];\n\t\tzoom: number;\n\t\tbearing: number;\n\t\tpitch: number;\n\t};\n\ttype FocusRouteOptions = {\n\t\tclosePopup?: () => void;\n\t\tpopupSnapshot?: PopupSnapshot | null;\n\t\tpopupRouteIdxs?: number[];\n\t\tpopupScrollTop?: number;\n\t};\n\n\tlet routesData = $state<RouteResponseData | null>(null);\n\tlet loadedBounds = $state<maplibregl.LngLatBounds | null>(null);\n\tlet loadedZoom = $state<number | null>(null);\n\tlet requestToken = 0;\n\tlet hoveredRouteIdx = $state<number | null>(null);\n\tlet downloadingRouteIdx = $state<number | null>(null);\n\tlet focusingRouteIdx = $state<number | null>(null);\n\tlet focusedRouteData = $state<RouteResponseData | null>(null);\n\tlet focusedMapView = $state<MapViewState | null>(null);\n\tlet focusedPopupSnapshot = $state<PopupSnapshot | null>(null);\n\tlet popupRouteIdxs = $state<number[]>([]);\n\tlet focusedPopupRouteIdxs = $state<number[]>([]);\n\tlet focusedPopupScrollTop = $state(0);\n\tlet pendingPopupScrollTop = $state<number | null>(null);\n\tlet popupScrollContainer = $state<HTMLDivElement>();\n\tlet focusRequestToken = 0;\n\tconst popupController: PopupController = {};\n\n\tconst stringToHash = (str: string) => {\n\t\tlet hash = 0;\n\t\tfor (let i = 0; i < str.length; i++) {\n\t\t\thash = str.charCodeAt(i) + ((hash << 5) - hash);\n\t\t}\n\t\treturn hash;\n\t};\n\n\tconst getRouteColor = (name: string) => {\n\t\tconst hash = stringToHash(name);\n\t\tconst h = Math.abs(hash % 360);\n\t\treturn colord({ h, s: 80, l: 60 }).toHex();\n\t};\n\n\ttype RouteFeatureProperties = {\n\t\tcolor: string;\n\t\tname: string;\n\t\trouteIndexes: string; // comma-separated because MapLibre stringifies properties\n\t};\n\n\tconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\n\tconst getRouteDisplayProps = (route: RouteInfo | undefined) => {\n\t\tif (!route) {\n\t\t\treturn { name: '', color: '#000000' };\n\t\t}\n\t\tconst shortNames = Array.from(new Set(route.transitRoutes.map((r) => r.shortName)));\n\t\tconst name = shortNames.join(', ');\n\t\tconst apiColor = route.transitRoutes.find((r) => r.color)?.color;\n\t\tconst color = apiColor ? `#${apiColor}` : getRouteColor(name);\n\t\treturn { name, color };\n\t};\n\n\tconst getTransitRouteName = (route: RouteInfo['transitRoutes'][number]) =>\n\t\troute.shortName || route.longName || '—';\n\n\tconst getRouteIdxsFromFeatures = (features: maplibregl.MapGeoJSONFeature[] = []) => {\n\t\tconst routeList = routesData?.routes ?? [];\n\t\tif (!routeList.length || !features.length) {\n\t\t\treturn [] as number[];\n\t\t}\n\n\t\tconst routeIdxs = new SvelteSet<number>();\n\t\tfor (const feature of features) {\n\t\t\tconst props = feature.properties as RouteFeatureProperties | null;\n\t\t\tconst routeIndexes = (props?.routeIndexes ?? '')\n\t\t\t\t.split(',')\n\t\t\t\t.map((value) => Number.parseInt(value));\n\t\t\tfor (const arrayIdx of routeIndexes) {\n\t\t\t\tconst route = routeList[arrayIdx];\n\t\t\t\tif (route) {\n\t\t\t\t\trouteIdxs.add(route.routeIdx);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn Array.from(routeIdxs).sort((a, b) => a - b);\n\t};\n\n\tconst getPolylineDisplayProps = (routePolyline: RoutePolyline, routeList: RouteInfo[]) => {\n\t\tconst rdp = getRouteDisplayProps(routeList[routePolyline.routeIndexes[0]]);\n\t\tconst color = routePolyline.colors.length ? `#${routePolyline.colors[0]}` : rdp.color;\n\t\treturn { name: rdp.name, color };\n\t};\n\n\tconst expandBounds = (value: maplibregl.LngLatBounds) => {\n\t\tconst sw = value.getSouthWest();\n\t\tconst ne = value.getNorthEast();\n\t\tconst padLng = (ne.lng - sw.lng) * FETCH_PADDING_RATIO;\n\t\tconst padLat = (ne.lat - sw.lat) * FETCH_PADDING_RATIO;\n\t\treturn new maplibregl.LngLatBounds(\n\t\t\t[clamp(sw.lng - padLng, -180, 180), clamp(sw.lat - padLat, -90, 90)],\n\t\t\t[clamp(ne.lng + padLng, -180, 180), clamp(ne.lat + padLat, -90, 90)]\n\t\t);\n\t};\n\n\tconst boundsContain = (outer: maplibregl.LngLatBounds | null, inner: maplibregl.LngLatBounds) =>\n\t\t!!outer && outer.contains(inner.getSouthWest()) && outer.contains(inner.getNorthEast());\n\n\tconst decodePolylines = (data: RouteResponseData | RouteDetailsPayload | null) =>\n\t\tdata\n\t\t\t? data.polylines.map((segment) =>\n\t\t\t\t\tpolyline\n\t\t\t\t\t\t.decode(segment.polyline.points, segment.polyline.precision)\n\t\t\t\t\t\t.map(([lat, lng]) => [lng, lat] as Position)\n\t\t\t\t)\n\t\t\t: [];\n\n\tconst getMapViewState = (): MapViewState | null => {\n\t\tif (!map) {\n\t\t\treturn null;\n\t\t}\n\t\tconst center = map.getCenter();\n\t\treturn {\n\t\t\tcenter: [center.lng, center.lat],\n\t\t\tzoom: map.getZoom(),\n\t\t\tbearing: map.getBearing(),\n\t\t\tpitch: map.getPitch()\n\t\t};\n\t};\n\n\tconst fitRouteData = (data: RouteResponseData) => {\n\t\tif (!map) {\n\t\t\treturn;\n\t\t}\n\t\tconst routeBounds = new maplibregl.LngLatBounds();\n\t\tlet hasCoordinates = false;\n\t\tfor (const polylineCoords of decodePolylines(data)) {\n\t\t\tfor (const coordinate of polylineCoords) {\n\t\t\t\trouteBounds.extend(coordinate as [number, number]);\n\t\t\t\thasCoordinates = true;\n\t\t\t}\n\t\t}\n\t\tif (!hasCoordinates) {\n\t\t\tfor (const stop of data.stops) {\n\t\t\t\trouteBounds.extend([stop.lon, stop.lat]);\n\t\t\t\thasCoordinates = true;\n\t\t\t}\n\t\t}\n\t\tif (hasCoordinates) {\n\t\t\tmap.fitBounds(routeBounds, {\n\t\t\t\tpadding: 80,\n\t\t\t\tduration: 0,\n\t\t\t\tmaxZoom: 16\n\t\t\t});\n\t\t}\n\t};\n\n\tconst applyFocusedRoute = (data: RouteResponseData, options: FocusRouteOptions = {}) => {\n\t\tfocusedMapView = getMapViewState();\n\t\tfocusedPopupSnapshot = options.popupSnapshot ?? null;\n\t\tfocusedPopupRouteIdxs = options.popupRouteIdxs ?? [];\n\t\tfocusedPopupScrollTop = options.popupScrollTop ?? 0;\n\t\thoveredRouteIdx = null;\n\t\tfocusedRouteData = data;\n\t\toptions.closePopup?.();\n\t\tfitRouteData(data);\n\t};\n\n\tconst clearFocusedRoute = () => {\n\t\tif (map && focusedMapView) {\n\t\t\tmap.jumpTo(focusedMapView);\n\t\t}\n\t\tfocusedRouteData = null;\n\t\thoveredRouteIdx = null;\n\t\tfocusedMapView = null;\n\t\tif (focusedPopupSnapshot) {\n\t\t\tpopupRouteIdxs = [...focusedPopupRouteIdxs];\n\t\t\tpopupController.open?.(focusedPopupSnapshot);\n\t\t\tpendingPopupScrollTop = focusedPopupScrollTop;\n\t\t}\n\t\tfocusedPopupSnapshot = null;\n\t\tfocusedPopupRouteIdxs = [];\n\t};\n\n\tconst loadFocusedRoute = async (routeIdx: number) => {\n\t\tconst { data, error, response } = await routeDetails({ query: { routeIdx } });\n\t\tif (error || !data) {\n\t\t\tthrow new Error(\n\t\t\t\t(typeof error === 'string' && error) ||\n\t\t\t\t\t(response?.status ? `HTTP ${response.status}` : 'route details request failed')\n\t\t\t);\n\t\t}\n\t\treturn data;\n\t};\n\n\tconst focusRoute = async (entry: RouteEntry, closePopup: () => void) => {\n\t\tconst token = ++focusRequestToken;\n\t\tfocusingRouteIdx = entry.route.routeIdx;\n\t\ttry {\n\t\t\tconst data = await loadFocusedRoute(entry.route.routeIdx);\n\t\t\tif (token !== focusRequestToken) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tapplyFocusedRoute(data, {\n\t\t\t\tclosePopup,\n\t\t\t\tpopupSnapshot: popupController.getSnapshot?.() ?? null,\n\t\t\t\tpopupRouteIdxs: [...popupRouteIdxs],\n\t\t\t\tpopupScrollTop: popupScrollContainer?.scrollTop ?? 0\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error('[Routes] failed to load full route details', error);\n\t\t} finally {\n\t\t\tif (token === focusRequestToken) {\n\t\t\t\tfocusingRouteIdx = null;\n\t\t\t}\n\t\t}\n\t};\n\n\tconst closeFocusedRoute = () => {\n\t\tclearFocusedRoute();\n\t};\n\n\tconst handleRouteRowKeydown = (\n\t\tevent: KeyboardEvent,\n\t\tentry: RouteEntry,\n\t\tclosePopup: () => void\n\t) => {\n\t\tif (event.key === 'Enter' || event.key === ' ') {\n\t\t\tevent.preventDefault();\n\t\t\tvoid focusRoute(entry, closePopup);\n\t\t}\n\t};\n\n\tconst formatRouteNames = (route: RouteInfo) =>\n\t\troute.transitRoutes.map((tr) => tr.shortName || tr.longName).join(', ') || '—';\n\n\tconst getStopKey = (stop: Place) => stop.stopId || `${stop.lat},${stop.lon}`;\n\n\tconst getProjectedSegmentDistance = (\n\t\tclickPoint: maplibregl.Point,\n\t\tstart: maplibregl.Point,\n\t\tend: maplibregl.Point\n\t) => {\n\t\tconst deltaX = end.x - start.x;\n\t\tconst deltaY = end.y - start.y;\n\t\tconst segmentLengthSq = deltaX * deltaX + deltaY * deltaY;\n\t\tif (segmentLengthSq === 0) {\n\t\t\tconst clickDeltaX = clickPoint.x - start.x;\n\t\t\tconst clickDeltaY = clickPoint.y - start.y;\n\t\t\treturn {\n\t\t\t\tdistanceSq: clickDeltaX * clickDeltaX + clickDeltaY * clickDeltaY,\n\t\t\t\tangleDeg: null\n\t\t\t};\n\t\t}\n\n\t\tconst projection =\n\t\t\t((clickPoint.x - start.x) * deltaX + (clickPoint.y - start.y) * deltaY) / segmentLengthSq;\n\t\tconst segmentFactor = clamp(projection, 0, 1);\n\t\tconst closestX = start.x + segmentFactor * deltaX;\n\t\tconst closestY = start.y + segmentFactor * deltaY;\n\t\tconst distanceX = clickPoint.x - closestX;\n\t\tconst distanceY = clickPoint.y - closestY;\n\n\t\treturn {\n\t\t\tdistanceSq: distanceX * distanceX + distanceY * distanceY,\n\t\t\tangleDeg: (Math.atan2(deltaY, deltaX) * 180) / Math.PI\n\t\t};\n\t};\n\n\tconst featureContainsRoute = (feature: maplibregl.MapGeoJSONFeature, arrayIdx: number) => {\n\t\tconst props = feature.properties as RouteFeatureProperties | null;\n\t\treturn (props?.routeIndexes ?? '').split(',').some((idx) => Number.parseInt(idx) === arrayIdx);\n\t};\n\n\tconst getFeatureDirectionAtPoint = (\n\t\tfeatures: maplibregl.MapGeoJSONFeature[],\n\t\tarrayIdx: number,\n\t\tclickPoint: maplibregl.PointLike\n\t): number | null => {\n\t\tlet bestDistanceSq = Number.POSITIVE_INFINITY;\n\t\tlet bestAngleDeg: number | null = null;\n\n\t\tfor (const feature of features) {\n\t\t\tif (!featureContainsRoute(feature, arrayIdx)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst geometry = feature.geometry;\n\t\t\tconst candidateLines =\n\t\t\t\tgeometry.type === 'LineString'\n\t\t\t\t\t? [geometry.coordinates as Position[]]\n\t\t\t\t\t: geometry.type === 'MultiLineString'\n\t\t\t\t\t\t? (geometry.coordinates as Position[][])\n\t\t\t\t\t\t: [];\n\n\t\t\tfor (const line of candidateLines) {\n\t\t\t\tif (line.length < 2 || !map) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst point = maplibregl.Point.convert(clickPoint);\n\t\t\t\tfor (let i = 1; i < line.length; i++) {\n\t\t\t\t\tconst startPoint = map.project(line[i - 1] as [number, number]);\n\t\t\t\t\tconst endPoint = map.project(line[i] as [number, number]);\n\t\t\t\t\tconst { distanceSq, angleDeg } = getProjectedSegmentDistance(point, startPoint, endPoint);\n\t\t\t\t\tif (distanceSq < bestDistanceSq) {\n\t\t\t\t\t\tbestDistanceSq = distanceSq;\n\t\t\t\t\t\tbestAngleDeg = angleDeg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn bestAngleDeg;\n\t};\n\n\tconst buildStopFeatures = (\n\t\tdata: RouteResponseData,\n\t\troute: RouteInfo,\n\t\tshowStopOrder: boolean\n\t): FeatureCollection<Point> => {\n\t\tconst stopMap = new SvelteMap<\n\t\t\tstring,\n\t\t\t{ lat: number; lon: number; name: string; color: string; stopNumber: string }\n\t\t>();\n\t\tconst { color } = getRouteDisplayProps(route);\n\t\tlet stopOrder = 1;\n\n\t\tfor (const segment of route.segments) {\n\t\t\tconst orderedStops = [segment.from, segment.to];\n\t\t\tfor (const stopIdx of orderedStops) {\n\t\t\t\tconst stop = data.stops[stopIdx];\n\t\t\t\tif (!stop) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst stopId = getStopKey(stop);\n\t\t\t\tif (!stopMap.has(stopId)) {\n\t\t\t\t\tstopMap.set(stopId, {\n\t\t\t\t\t\tlat: stop.lat,\n\t\t\t\t\t\tlon: stop.lon,\n\t\t\t\t\t\tname: stop.name,\n\t\t\t\t\t\tcolor,\n\t\t\t\t\t\tstopNumber: showStopOrder ? `${stopOrder}` : ''\n\t\t\t\t\t});\n\t\t\t\t\t++stopOrder;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: Array.from(stopMap.values()).map((stop) => ({\n\t\t\t\ttype: 'Feature',\n\t\t\t\tgeometry: {\n\t\t\t\t\ttype: 'Point',\n\t\t\t\t\tcoordinates: [stop.lon, stop.lat]\n\t\t\t\t},\n\t\t\t\tproperties: {\n\t\t\t\t\tname: stop.name,\n\t\t\t\t\tcolor: stop.color,\n\t\t\t\t\tstopNumber: stop.stopNumber\n\t\t\t\t}\n\t\t\t}))\n\t\t};\n\t};\n\n\tconst fetchRoutes = async (mapBounds: maplibregl.LngLatBounds) => {\n\t\tconst expandedBounds = expandBounds(mapBounds);\n\t\tconst isBoundsContained = loadedBounds && boundsContain(loadedBounds, mapBounds);\n\t\tconst isZoomSufficient =\n\t\t\t!routesData?.zoomFiltered || Math.floor(zoom) <= Math.floor(loadedZoom ?? 0);\n\n\t\tif (isBoundsContained && isZoomSufficient) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst token = ++requestToken;\n\n\t\tconst requestWithBounds = async (requestBounds: maplibregl.LngLatBounds) => {\n\t\t\tconst max = lngLatToStr(requestBounds.getNorthWest());\n\t\t\tconst min = lngLatToStr(requestBounds.getSouthEast());\n\t\t\tconsole.debug('[Routes] requesting routes', { min, max, zoom });\n\t\t\treturn { ...(await routes({ query: { max, min, zoom } })), requestBounds };\n\t\t};\n\n\t\tlet { data, error, response, requestBounds } = await requestWithBounds(expandedBounds);\n\n\t\tif (token !== requestToken) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (error && response?.status === 422) {\n\t\t\tconsole.debug(\n\t\t\t\t'[Routes] routes request returned 422 for expanded bounds, retrying with map bounds'\n\t\t\t);\n\t\t\t({ data, error, response, requestBounds } = await requestWithBounds(mapBounds));\n\t\t\tif (token !== requestToken) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (error || !data) {\n\t\t\tconsole.error('[Routes] routes request failed', error);\n\t\t\treturn;\n\t\t}\n\t\tconsole.debug(\n\t\t\t`[Routes] received ${data.routes.length} routes, ${data.polylines.length} polylines, ${data.stops.length} stops, zoomFiltered=${data.zoomFiltered}`\n\t\t);\n\n\t\troutesData = data;\n\t\tloadedBounds = requestBounds;\n\t\tloadedZoom = zoom;\n\t};\n\n\t$effect(() => {\n\t\tif (map && bounds && !focusedRouteData) {\n\t\t\tconst b = maplibregl.LngLatBounds.convert(bounds);\n\t\t\tfetchRoutes(b);\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (popupScrollContainer && pendingPopupScrollTop !== null) {\n\t\t\tpopupScrollContainer.scrollTop = pendingPopupScrollTop;\n\t\t\tpendingPopupScrollTop = null;\n\t\t}\n\t});\n\n\tlet decodedPolylines = $derived.by(() => decodePolylines(routesData));\n\tlet focusedDecodedPolylines = $derived.by(() => decodePolylines(focusedRouteData));\n\tlet routeIndexesByRouteIdx = $derived.by(\n\t\t() => new Map((routesData?.routes ?? []).map((route, arrayIdx) => [route.routeIdx, arrayIdx]))\n\t);\n\tlet hoveredRoute = $derived.by(() => {\n\t\tif (hoveredRouteIdx === null || focusedRouteData) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst arrayIdx = routeIndexesByRouteIdx.get(hoveredRouteIdx);\n\t\tif (arrayIdx === undefined || !routesData) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn {\n\t\t\troute: routesData.routes[arrayIdx],\n\t\t\tarrayIdx\n\t\t};\n\t});\n\n\tlet routeFeatures = $derived.by((): FeatureCollection<LineString> => {\n\t\tif (!routesData || focusedRouteData) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\t\tconst data = routesData;\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: data.polylines.map((segment, polylineIdx) => {\n\t\t\t\tconst pdp = getPolylineDisplayProps(segment, data.routes);\n\t\t\t\treturn {\n\t\t\t\t\ttype: 'Feature',\n\t\t\t\t\tgeometry: {\n\t\t\t\t\t\ttype: 'LineString',\n\t\t\t\t\t\tcoordinates: decodedPolylines[polylineIdx] ?? []\n\t\t\t\t\t},\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tcolor: pdp.color,\n\t\t\t\t\t\tname: pdp.name,\n\t\t\t\t\t\trouteIndexes: segment.routeIndexes.join(',')\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t})\n\t\t};\n\t});\n\n\tlet hoverRouteFeatures = $derived.by((): FeatureCollection<LineString> => {\n\t\tif (!hoveredRoute) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\t\tconst { route, arrayIdx } = hoveredRoute;\n\t\tconst { name, color } = getRouteDisplayProps(route);\n\t\tconst { outlineColor, chevronColor } = getDecorativeColors(color);\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: route.segments.map((segment) => ({\n\t\t\t\ttype: 'Feature',\n\t\t\t\tgeometry: {\n\t\t\t\t\ttype: 'LineString',\n\t\t\t\t\tcoordinates: decodedPolylines[segment.polyline] ?? []\n\t\t\t\t},\n\t\t\t\tproperties: {\n\t\t\t\t\tcolor,\n\t\t\t\t\toutlineColor,\n\t\t\t\t\tchevronColor,\n\t\t\t\t\tname,\n\t\t\t\t\tarrayIdx\n\t\t\t\t}\n\t\t\t}))\n\t\t};\n\t});\n\n\tlet hoverStopFeatures = $derived.by((): FeatureCollection<Point> => {\n\t\tif (!hoveredRoute) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\n\t\treturn buildStopFeatures(routesData!, hoveredRoute.route, false);\n\t});\n\n\tlet focusedRouteFeatures = $derived.by((): FeatureCollection<LineString> => {\n\t\tif (!focusedRouteData) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\t\tconst route = focusedRouteData.routes[0];\n\t\tif (!route) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\t\tconst { name, color } = getRouteDisplayProps(route);\n\t\tconst { outlineColor, chevronColor } = getDecorativeColors(color);\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: route.segments.map((segment) => ({\n\t\t\t\ttype: 'Feature',\n\t\t\t\tgeometry: {\n\t\t\t\t\ttype: 'LineString',\n\t\t\t\t\tcoordinates: focusedDecodedPolylines[segment.polyline] ?? []\n\t\t\t\t},\n\t\t\t\tproperties: {\n\t\t\t\t\tcolor,\n\t\t\t\t\toutlineColor,\n\t\t\t\t\tchevronColor,\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}))\n\t\t};\n\t});\n\n\tlet focusedStopFeatures = $derived.by((): FeatureCollection<Point> => {\n\t\tif (!focusedRouteData) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\t\tconst data = focusedRouteData;\n\t\tconst route = data.routes[0];\n\t\tif (!route) {\n\t\t\treturn { type: 'FeatureCollection', features: [] };\n\t\t}\n\n\t\treturn buildStopFeatures(data, route, true);\n\t});\n\n\tlet focusedRoute = $derived.by(() => focusedRouteData?.routes[0] ?? null);\n\n\tpopupController.onSnapshotChange = (snapshot) => {\n\t\tpopupRouteIdxs = getRouteIdxsFromFeatures(snapshot?.features);\n\t};\n\n\tconst getRouteModeName = (mode: RouteInfo['mode']) => getModeName({ mode } as Leg);\n\n\tconst getPopupPoint = (lngLat: maplibregl.LngLatLike, fallbackPoint: maplibregl.PointLike) => {\n\t\tif (!map) {\n\t\t\treturn fallbackPoint;\n\t\t}\n\n\t\treturn map.project(maplibregl.LngLat.convert(lngLat));\n\t};\n\n\tconst getRouteFeaturesAtPoint = (\n\t\tlngLat: maplibregl.LngLatLike,\n\t\tfallbackPoint: maplibregl.PointLike\n\t) => {\n\t\tconst queried = map?.queryRenderedFeatures(getPopupPoint(lngLat, fallbackPoint), {\n\t\t\tlayers: ['routes-layer']\n\t\t});\n\t\treturn (queried ?? []) as maplibregl.MapGeoJSONFeature[];\n\t};\n\n\tconst getDisplayedRouteIdxs = (\n\t\tfeatures: maplibregl.MapGeoJSONFeature[] | undefined,\n\t\trouteFeaturesAtPoint: maplibregl.MapGeoJSONFeature[]\n\t) =>\n\t\tpopupRouteIdxs.length\n\t\t\t? popupRouteIdxs\n\t\t\t: getRouteIdxsFromFeatures(features?.length ? features : routeFeaturesAtPoint);\n\n\tconst getRoutesFromRouteIdxs = (\n\t\trouteIdxs: number[],\n\t\tfeaturesAtPoint: maplibregl.MapGeoJSONFeature[],\n\t\tclickPoint: maplibregl.PointLike\n\t) => {\n\t\tconst routeList = routesData?.routes ?? [];\n\t\tif (!routeList.length) {\n\t\t\treturn [] as RouteEntry[];\n\t\t}\n\n\t\treturn routeIdxs\n\t\t\t.map((routeIdx) => {\n\t\t\t\tconst arrayIdx = routeIndexesByRouteIdx.get(routeIdx);\n\t\t\t\tif (arrayIdx === undefined) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst route = routeList[arrayIdx];\n\t\t\t\tconst featureDirection = getFeatureDirectionAtPoint(featuresAtPoint, arrayIdx, clickPoint);\n\t\t\t\treturn {\n\t\t\t\t\troute,\n\t\t\t\t\tarrayIdx,\n\t\t\t\t\tcolor: getRouteDisplayProps(route).color,\n\t\t\t\t\tdirectionDeg: featureDirection\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((entry): entry is RouteEntry => entry !== null)\n\t\t\t.sort((a, b) => a.route.routeIdx - b.route.routeIdx);\n\t};\n\n\tfunction getUrlBase(url: string): string {\n\t\tconst { origin, pathname } = new URL(url);\n\t\treturn origin + pathname.slice(0, pathname.lastIndexOf('/') + 1);\n\t}\n\n\tconst getDownloadFilename = (contentDisposition: string | null, fallback: string): string => {\n\t\tif (!contentDisposition) {\n\t\t\treturn fallback;\n\t\t}\n\n\t\tconst utf8Match = /filename\\*=UTF-8''([^;]+)/i.exec(contentDisposition);\n\t\tif (utf8Match?.[1]) {\n\t\t\ttry {\n\t\t\t\treturn decodeURIComponent(utf8Match[1]).trim();\n\t\t\t} catch {} // eslint-disable-line no-empty\n\t\t}\n\n\t\tconst plainMatch = /filename=\"?([^\";]+)\"?/i.exec(contentDisposition);\n\t\treturn plainMatch?.[1]?.trim() || fallback;\n\t};\n\n\tconst downloadRouteDebug = async (routeIdx: number) => {\n\t\tif (downloadingRouteIdx === routeIdx) {\n\t\t\treturn;\n\t\t}\n\n\t\tdownloadingRouteIdx = routeIdx;\n\t\ttry {\n\t\t\tconst apiBaseUrl = getUrlBase(\n\t\t\t\tclient.getConfig().baseUrl\n\t\t\t\t\t? client.getConfig().baseUrl + '/'\n\t\t\t\t\t: window.location.origin + window.location.pathname\n\t\t\t);\n\t\t\tconst response = await fetch(`${apiBaseUrl}api/experimental/shapes-debug/${routeIdx}`);\n\t\t\tif (!response.ok) {\n\t\t\t\tconst body = await response.text();\n\t\t\t\tthrow new Error(body || `HTTP ${response.status}`);\n\t\t\t}\n\n\t\t\tconst blob = await response.blob();\n\t\t\tconst filename = getDownloadFilename(\n\t\t\t\tresponse.headers.get('content-disposition'),\n\t\t\t\t`r_${routeIdx}.json.gz`\n\t\t\t);\n\n\t\t\tconst objectUrl = URL.createObjectURL(blob);\n\t\t\tconst a = document.createElement('a');\n\t\t\ta.href = objectUrl;\n\t\t\ta.download = filename;\n\t\t\tdocument.body.appendChild(a);\n\t\t\ta.click();\n\t\t\ta.remove();\n\t\t\tURL.revokeObjectURL(objectUrl);\n\t\t} catch (error) {\n\t\t\tconsole.error('[Routes] failed to download shapes debug data', error);\n\t\t} finally {\n\t\t\tdownloadingRouteIdx = null;\n\t\t}\n\t};\n</script>\n\n{#snippet routesPopup(\n\tevent: maplibregl.MapMouseEvent,\n\tclosePopup: () => void,\n\tfeatures: maplibregl.MapGeoJSONFeature[] | undefined\n)}\n\t{@const popupPoint = getPopupPoint(event.lngLat, event.point)}\n\t{@const popupFeatures = features?.length\n\t\t? features\n\t\t: getRouteFeaturesAtPoint(event.lngLat, event.point)}\n\t{@const displayedRouteIdxs = getDisplayedRouteIdxs(features, popupFeatures)}\n\t{@const routesAtPoint = getRoutesFromRouteIdxs(displayedRouteIdxs, popupFeatures, popupPoint)}\n\t<div\n\t\tbind:this={popupScrollContainer}\n\t\tclass=\"w-fit max-w-[min(88vw,72rem)] max-h-[min(70vh,32rem)] overflow-y-auto pr-1 text-sm\"\n\t\trole=\"dialog\"\n\t\ttabindex=\"0\"\n\t\tonmouseleave={() => {\n\t\t\thoveredRouteIdx = null;\n\t\t}}\n\t>\n\t\t<div class=\"mb-2 font-semibold\">{t.routes(routesAtPoint.length)}</div>\n\t\t<table class=\"w-fit table-auto border-separate border-spacing-y-1 text-sm\">\n\t\t\t<thead class=\"text-xs uppercase text-muted-foreground\">\n\t\t\t\t<tr>\n\t\t\t\t\t<th class=\"w-4 px-1 py-1\"></th>\n\t\t\t\t\t<th class=\"w-4 px-1 py-1\"></th>\n\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">Index</th>\n\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">Mode</th>\n\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">ID</th>\n\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">Name</th>\n\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">Stops</th>\n\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">Source</th>\n\t\t\t\t\t{#if shapesDebugEnabled}\n\t\t\t\t\t\t<th class=\"px-1 py-1 text-left font-medium\">Debug Data</th>\n\t\t\t\t\t{/if}\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t{#each routesAtPoint as entry (entry.route.routeIdx)}\n\t\t\t\t\t<tr\n\t\t\t\t\t\tclass=\"group cursor-pointer align-top hover:bg-muted/80 focus-within:bg-muted/80\"\n\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\ttabindex=\"0\"\n\t\t\t\t\t\taria-label={`Focus route ${entry.route.routeIdx}`}\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tvoid focusRoute(entry, closePopup);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonkeydown={(keyboardEvent) => {\n\t\t\t\t\t\t\thandleRouteRowKeydown(keyboardEvent, entry, closePopup);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonmouseenter={() => {\n\t\t\t\t\t\t\thoveredRouteIdx = entry.route.routeIdx;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonmouseleave={() => {\n\t\t\t\t\t\t\thoveredRouteIdx = null;\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<td class=\"px-1 py-1 align-top\">\n\t\t\t\t\t\t\t{#if focusingRouteIdx === entry.route.routeIdx}\n\t\t\t\t\t\t\t\t<LoaderCircle class=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" />\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"inline-block h-2.5 w-2.5 rounded-full\"\n\t\t\t\t\t\t\t\t\tstyle=\"background: {entry.color}\"\n\t\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-2 align-top\">\n\t\t\t\t\t\t\t{#if entry.directionDeg !== null}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"inline-flex h-5 w-5 items-center justify-center rounded-full border border-border/70 bg-background/90 text-muted-foreground\"\n\t\t\t\t\t\t\t\t\ttitle=\"Direction at clicked point\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<ArrowRight\n\t\t\t\t\t\t\t\t\t\tclass=\"h-3.5 w-3.5\"\n\t\t\t\t\t\t\t\t\t\tstyle={`transform: rotate(${entry.directionDeg}deg);`}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<span class=\"text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-2 align-top whitespace-nowrap\">{entry.route.routeIdx}</td>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-2 align-top whitespace-nowrap\"\n\t\t\t\t\t\t\t>{getRouteModeName(entry.route.mode)}</td\n\t\t\t\t\t\t>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-2 align-top\">\n\t\t\t\t\t\t\t{#if entry.route.transitRoutes.length}\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col items-start gap-1\">\n\t\t\t\t\t\t\t\t\t{#each entry.route.transitRoutes as tr, i (tr.id + i)}\n\t\t\t\t\t\t\t\t\t\t<span class=\"whitespace-nowrap rounded-md bg-muted/60 px-2 py-1 leading-none\">\n\t\t\t\t\t\t\t\t\t\t\t{tr.id}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<span class=\"text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-1 align-top\">\n\t\t\t\t\t\t\t{#if entry.route.transitRoutes.length}\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col items-start gap-1\">\n\t\t\t\t\t\t\t\t\t{#each entry.route.transitRoutes as tr, i (tr.id + i)}\n\t\t\t\t\t\t\t\t\t\t<span class=\"whitespace-nowrap rounded-md bg-muted/60 px-2 py-1 leading-none\">\n\t\t\t\t\t\t\t\t\t\t\t{getTransitRouteName(tr)}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<span class=\"text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-1 align-top whitespace-nowrap\">{entry.route.numStops}</td>\n\t\t\t\t\t\t<td class=\"px-1 py-1 pr-2 align-top whitespace-nowrap\">{entry.route.pathSource}</td>\n\t\t\t\t\t\t{#if shapesDebugEnabled}\n\t\t\t\t\t\t\t<td class=\"px-1 py-1 align-top\">\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclass=\"inline-flex h-6 min-w-[7.6rem] items-center justify-center gap-1 rounded border border-border/80 bg-background/85 px-2 text-[11px] leading-none text-foreground transition-colors hover:bg-background disabled:cursor-wait disabled:opacity-70\"\n\t\t\t\t\t\t\t\t\tdisabled={downloadingRouteIdx === entry.route.routeIdx}\n\t\t\t\t\t\t\t\t\taria-busy={downloadingRouteIdx === entry.route.routeIdx}\n\t\t\t\t\t\t\t\t\tonclick={(event) => {\n\t\t\t\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t\t\t\t\tvoid downloadRouteDebug(entry.route.routeIdx);\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{#if downloadingRouteIdx === entry.route.routeIdx}\n\t\t\t\t\t\t\t\t\t\t<LoaderCircle class=\"h-3.5 w-3.5 animate-spin\" />\n\t\t\t\t\t\t\t\t\t\t<span>Generating...</span>\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t<Download class=\"h-3.5 w-3.5\" />\n\t\t\t\t\t\t\t\t\t\t<span>Debug</span>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</tr>\n\t\t\t\t{/each}\n\t\t\t</tbody>\n\t\t</table>\n\t</div>\n{/snippet}\n\n<GeoJSON id=\"routes-source\" data={routeFeatures}>\n\t<Layer\n\t\tid=\"routes-layer\"\n\t\ttype=\"line\"\n\t\tfilter={['all']}\n\t\tpaint={{\n\t\t\t'line-color': ['get', 'color'],\n\t\t\t'line-width': 3,\n\t\t\t'line-opacity': 0.7\n\t\t}}\n\t\tlayout={{\n\t\t\t'line-cap': 'round',\n\t\t\t'line-join': 'round'\n\t\t}}\n\t>\n\t\t<Popup trigger=\"click\" children={routesPopup} controller={popupController} />\n\t</Layer>\n\t<Layer\n\t\tid=\"routes-names\"\n\t\ttype=\"symbol\"\n\t\tfilter={['all']}\n\t\tlayout={{\n\t\t\t'symbol-placement': 'line',\n\t\t\t'text-field': ['get', 'name'],\n\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t'text-size': 12,\n\t\t\t'text-max-angle': 30,\n\t\t\t'symbol-sort-key': -100,\n\t\t\t'text-allow-overlap': false\n\t\t}}\n\t\tpaint={{\n\t\t\t'text-color': '#000000',\n\t\t\t'text-halo-color': '#ffffff',\n\t\t\t'text-halo-width': 2\n\t\t}}\n\t/>\n</GeoJSON>\n\n<GeoJSON id=\"routes-focused-source\" data={focusedRouteFeatures}>\n\t<Layer\n\t\tid=\"routes-focused-glow\"\n\t\ttype=\"line\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tpaint={{\n\t\t\t'line-color': ['get', 'color'],\n\t\t\t'line-width': 20,\n\t\t\t'line-opacity': 0.22,\n\t\t\t'line-blur': 6\n\t\t}}\n\t\tlayout={{\n\t\t\t'line-cap': 'round',\n\t\t\t'line-join': 'round'\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-focused-outline\"\n\t\ttype=\"line\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tpaint={{\n\t\t\t'line-color': ['get', 'outlineColor'],\n\t\t\t'line-width': 12,\n\t\t\t'line-opacity': 0.92\n\t\t}}\n\t\tlayout={{\n\t\t\t'line-cap': 'round',\n\t\t\t'line-join': 'round'\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-focused\"\n\t\ttype=\"line\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tpaint={{\n\t\t\t'line-color': ['get', 'color'],\n\t\t\t'line-width': 7,\n\t\t\t'line-opacity': 0.98\n\t\t}}\n\t\tlayout={{\n\t\t\t'line-cap': 'round',\n\t\t\t'line-join': 'round'\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-focused-chevrons\"\n\t\ttype=\"symbol\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tlayout={{\n\t\t\t'symbol-placement': 'line',\n\t\t\t'symbol-spacing': 50,\n\t\t\t'text-field': '›',\n\t\t\t'text-size': 24,\n\t\t\t'text-font': ['Noto Sans Bold'],\n\t\t\t'text-keep-upright': false,\n\t\t\t'text-allow-overlap': true,\n\t\t\t'text-rotation-alignment': 'map',\n\t\t\t'text-offset': [0, -0.1]\n\t\t}}\n\t\tpaint={{\n\t\t\t'text-color': ['get', 'chevronColor'],\n\t\t\t'text-opacity': 0.88,\n\t\t\t'text-halo-color': ['get', 'outlineColor'],\n\t\t\t'text-halo-width': 0.5,\n\t\t\t'text-halo-blur': 0.2\n\t\t}}\n\t/>\n</GeoJSON>\n\n<GeoJSON id=\"routes-focused-stops-source\" data={focusedStopFeatures}>\n\t<Layer\n\t\tid=\"routes-focused-stops\"\n\t\ttype=\"circle\"\n\t\tlayout={{}}\n\t\tfilter={['all']}\n\t\tpaint={{\n\t\t\t'circle-radius': 10,\n\t\t\t'circle-color': 'white',\n\t\t\t'circle-stroke-width': 4,\n\t\t\t'circle-stroke-color': ['get', 'color']\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-focused-stops-order\"\n\t\ttype=\"symbol\"\n\t\tfilter={['all']}\n\t\tlayout={{\n\t\t\t'text-field': ['get', 'stopNumber'],\n\t\t\t'text-font': ['Noto Sans Bold'],\n\t\t\t'text-size': 10,\n\t\t\t'text-allow-overlap': true,\n\t\t\t'text-ignore-placement': true\n\t\t}}\n\t\tpaint={{\n\t\t\t'text-color': '#111827'\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-focused-stops-names\"\n\t\ttype=\"symbol\"\n\t\tfilter={['all']}\n\t\tlayout={{\n\t\t\t'text-field': ['get', 'name'],\n\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t'text-size': 12,\n\t\t\t'text-offset': [0, 1.45],\n\t\t\t'text-anchor': 'top'\n\t\t}}\n\t\tpaint={{\n\t\t\t'text-color': '#000000',\n\t\t\t'text-halo-color': '#ffffff',\n\t\t\t'text-halo-width': 2.2\n\t\t}}\n\t/>\n</GeoJSON>\n\n<GeoJSON id=\"routes-hover-source\" data={hoverRouteFeatures}>\n\t<Layer\n\t\tid=\"routes-hover-outline\"\n\t\ttype=\"line\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tpaint={{\n\t\t\t'line-color': '#ffffff',\n\t\t\t'line-width': 20,\n\t\t\t'line-opacity': 0.9\n\t\t}}\n\t\tlayout={{\n\t\t\t'line-cap': 'round',\n\t\t\t'line-join': 'round'\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-hover\"\n\t\ttype=\"line\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tpaint={{\n\t\t\t'line-color': ['get', 'color'],\n\t\t\t'line-width': 14,\n\t\t\t'line-opacity': 0.95\n\t\t}}\n\t\tlayout={{\n\t\t\t'line-cap': 'round',\n\t\t\t'line-join': 'round'\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-hover-chevrons\"\n\t\ttype=\"symbol\"\n\t\tfilter={['all']}\n\t\tbeforeLayerId=\"routes-names\"\n\t\tlayout={{\n\t\t\t'symbol-placement': 'line',\n\t\t\t'symbol-spacing': 40,\n\t\t\t'text-field': '›',\n\t\t\t'text-size': 24,\n\t\t\t'text-font': ['Noto Sans Bold'],\n\t\t\t'text-keep-upright': false,\n\t\t\t'text-allow-overlap': true,\n\t\t\t'text-rotation-alignment': 'map',\n\t\t\t'text-offset': [0, -0.1]\n\t\t}}\n\t\tpaint={{\n\t\t\t'text-color': ['get', 'chevronColor'],\n\t\t\t'text-opacity': 0.85,\n\t\t\t'text-halo-color': ['get', 'outlineColor'],\n\t\t\t'text-halo-width': 0.5,\n\t\t\t'text-halo-blur': 0.2\n\t\t}}\n\t/>\n</GeoJSON>\n\n<GeoJSON id=\"routes-hover-stops-source\" data={hoverStopFeatures}>\n\t<Layer\n\t\tid=\"routes-hover-stops\"\n\t\ttype=\"circle\"\n\t\tlayout={{}}\n\t\tfilter={['all']}\n\t\tpaint={{\n\t\t\t'circle-radius': 5,\n\t\t\t'circle-color': 'white',\n\t\t\t'circle-stroke-width': 4,\n\t\t\t'circle-stroke-color': ['get', 'color']\n\t\t}}\n\t/>\n\t<Layer\n\t\tid=\"routes-hover-stops-names\"\n\t\ttype=\"symbol\"\n\t\tfilter={['all']}\n\t\tlayout={{\n\t\t\t'text-field': ['get', 'name'],\n\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t'text-size': 12,\n\t\t\t'text-offset': [0, 1],\n\t\t\t'text-anchor': 'top'\n\t\t}}\n\t\tpaint={{\n\t\t\t'text-color': '#000000',\n\t\t\t'text-halo-color': '#ffffff',\n\t\t\t'text-halo-width': 2\n\t\t}}\n\t/>\n</GeoJSON>\n\n{#if focusedRoute}\n\t{@const focusedDisplay = getRouteDisplayProps(focusedRoute)}\n\t<Control position=\"top-right\" class=\"mt-24 max-w-[min(24rem,calc(100vw-5.5rem))]\">\n\t\t<div class=\"rounded-xl border border-border/80 bg-background/95 p-3 shadow-xl backdrop-blur\">\n\t\t\t<div class=\"flex items-center justify-between gap-3\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"inline-flex shrink-0 items-center gap-2 rounded-md border border-border/80 bg-background px-3 py-2 text-xs font-medium transition-colors hover:bg-muted\"\n\t\t\t\t\tonclick={closeFocusedRoute}\n\t\t\t\t>\n\t\t\t\t\t<ArrowLeft class=\"h-3.5 w-3.5\" />\n\t\t\t\t\t<span class=\"sr-only\">Back</span>\n\t\t\t\t</button>\n\t\t\t\t<div class=\"min-w-0 flex-1\">\n\t\t\t\t\t<div class=\"flex items-center gap-2\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclass=\"inline-block h-2.5 w-2.5 rounded-full\"\n\t\t\t\t\t\t\tstyle=\"background: {focusedDisplay.color}\"\n\t\t\t\t\t\t></span>\n\t\t\t\t\t\t<div class=\"text-sm font-semibold leading-tight truncate\">\n\t\t\t\t\t\t\t{formatRouteNames(focusedRoute)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{#if shapesDebugEnabled}\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"inline-flex shrink-0 items-center gap-2 rounded-md border border-border/80 bg-background px-3 py-2 text-xs font-medium transition-colors hover:bg-muted disabled:cursor-wait disabled:opacity-70\"\n\t\t\t\t\t\tdisabled={downloadingRouteIdx === focusedRoute.routeIdx}\n\t\t\t\t\t\taria-busy={downloadingRouteIdx === focusedRoute.routeIdx}\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tvoid downloadRouteDebug(focusedRoute.routeIdx);\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if downloadingRouteIdx === focusedRoute.routeIdx}\n\t\t\t\t\t\t\t<LoaderCircle class=\"h-3.5 w-3.5 animate-spin\" />\n\t\t\t\t\t\t\t<span class=\"sr-only\">Generating...</span>\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t<Download class=\"h-3.5 w-3.5\" />\n\t\t\t\t\t\t\t<span class=\"sr-only\">Debug</span>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</button>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t\t<div class=\"mt-3 grid grid-cols-[auto,1fr] gap-x-3 gap-y-1 text-xs leading-5\">\n\t\t\t\t<div class=\"text-muted-foreground\">Index</div>\n\t\t\t\t<div>{focusedRoute.routeIdx}</div>\n\t\t\t\t<div class=\"text-muted-foreground\">Mode</div>\n\t\t\t\t<div>{getRouteModeName(focusedRoute.mode)}</div>\n\t\t\t\t<div class=\"text-muted-foreground\">IDs</div>\n\t\t\t\t<div class=\"break-words\">\n\t\t\t\t\t{#if focusedRoute.transitRoutes.length}\n\t\t\t\t\t\t{#each focusedRoute.transitRoutes as transitRoute, i (transitRoute.id + i)}\n\t\t\t\t\t\t\t<div>{transitRoute.id}</div>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<div>—</div>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"text-muted-foreground\">Names</div>\n\t\t\t\t<div class=\"break-words\">\n\t\t\t\t\t{#if focusedRoute.transitRoutes.length}\n\t\t\t\t\t\t{#each focusedRoute.transitRoutes as transitRoute, i (transitRoute.id + i)}\n\t\t\t\t\t\t\t<div>{transitRoute.shortName || transitRoute.longName}</div>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<div>—</div>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"text-muted-foreground\">Stops</div>\n\t\t\t\t<div>{focusedRoute.numStops}</div>\n\t\t\t\t<div class=\"text-muted-foreground\">Source</div>\n\t\t\t\t<div>{focusedRoute.pathSource}</div>\n\t\t\t</div>\n\t\t</div>\n\t</Control>\n{/if}\n"
  },
  {
    "path": "ui/src/lib/map/shield.ts",
    "content": "import { browser } from '$app/environment';\nimport type { StyleImageMetadata } from 'maplibre-gl';\n\nclass ShieldOptions {\n\tfill!: string;\n\tstroke!: string;\n}\n\nexport function createShield(opt: ShieldOptions): [ImageData, Partial<StyleImageMetadata>] {\n\tif (!browser) {\n\t\tthrow 'not supported';\n\t}\n\n\tconst d = 32;\n\n\tconst cv = document.createElement('canvas');\n\tcv.width = d;\n\tcv.height = d;\n\tconst ctx = cv.getContext('2d')!;\n\n\t// coord of the line (front = near zero, back = opposite)\n\tconst l_front = 1;\n\tconst l_back = d - 1;\n\n\t// coord start of the arc\n\tconst lr_front = l_front + 2;\n\tconst lr_back = l_back - 2;\n\n\t// control point of the arc\n\tconst lp_front = l_front + 1;\n\tconst lp_back = l_back - 1;\n\n\tconst p = new Path2D();\n\tp.moveTo(lr_front, l_front);\n\n\t// top line\n\tp.lineTo(lr_back, l_front);\n\t// top right corner\n\tp.bezierCurveTo(lp_back, lp_front, lp_back, lp_front, l_back, lr_front);\n\t// right line\n\tp.lineTo(l_back, lr_back);\n\t// bottom right corner\n\tp.bezierCurveTo(lp_back, lp_back, lp_back, lp_back, lr_back, l_back);\n\t// bottom line\n\tp.lineTo(lr_front, l_back);\n\t// bottom left corner\n\tp.bezierCurveTo(lp_front, lp_back, lp_front, lp_back, l_front, lr_back);\n\t// left line\n\tp.lineTo(l_front, lr_front);\n\t// top left corner\n\tp.bezierCurveTo(lp_front, lp_front, lp_front, lp_front, lr_front, l_front);\n\n\tp.closePath();\n\n\tctx.fillStyle = opt.fill;\n\tctx.fill(p);\n\tctx.strokeStyle = opt.stroke;\n\tctx.stroke(p);\n\n\treturn [\n\t\tctx.getImageData(0, 0, d, d),\n\t\t{\n\t\t\tcontent: [lr_front, lr_front, lr_back, lr_back],\n\t\t\tstretchX: [[lr_front, lr_back]],\n\t\t\tstretchY: [[lr_front, lr_back]]\n\t\t}\n\t];\n}\n"
  },
  {
    "path": "ui/src/lib/map/stops/StopsGeoJSON.svelte",
    "content": "<script lang=\"ts\">\n\timport Layer from '$lib/map/Layer.svelte';\n\timport GeoJSON from '$lib/map/GeoJSON.svelte';\n\timport type { Itinerary, Leg } from '@motis-project/motis-client';\n\timport { onClickStop } from '../../utils';\n\timport { getColor } from '../../modeStyle';\n\n\tlet {\n\t\titinerary = $bindable(),\n\t\ttheme\n\t}: {\n\t\titinerary: Itinerary;\n\t\ttheme: 'dark' | 'light';\n\t} = $props();\n\n\tfunction stopsToGeoJSON(legs: Leg[]): GeoJSON.GeoJSON {\n\t\treturn {\n\t\t\ttype: 'FeatureCollection',\n\t\t\tfeatures: legs\n\t\t\t\t.filter((l) => {\n\t\t\t\t\treturn l.mode !== 'WALK' && l.mode !== 'BIKE' && l.mode !== 'CAR';\n\t\t\t\t})\n\t\t\t\t.flatMap((l) => {\n\t\t\t\t\tconst stops = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'Feature' as const,\n\t\t\t\t\t\t\tgeometry: { type: 'Point' as const, coordinates: [l.from.lon, l.from.lat] },\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tstopId: l.from.stopId,\n\t\t\t\t\t\t\t\tname: l.from.name,\n\t\t\t\t\t\t\t\ttime: l.from.arrival ?? l.from.departure,\n\t\t\t\t\t\t\t\tcolor: getColor(l)[0]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: 'Feature' as const,\n\t\t\t\t\t\t\tgeometry: { type: 'Point' as const, coordinates: [l.to.lon, l.to.lat] },\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tstopId: l.to.stopId,\n\t\t\t\t\t\t\t\tname: l.to.name,\n\t\t\t\t\t\t\t\ttime: l.to.arrival,\n\t\t\t\t\t\t\t\tcolor: getColor(l)[0]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t];\n\t\t\t\t\tconst intermediateStops = l.intermediateStops\n\t\t\t\t\t\t? l.intermediateStops.map((s) => ({\n\t\t\t\t\t\t\t\ttype: 'Feature' as const,\n\t\t\t\t\t\t\t\tgeometry: {\n\t\t\t\t\t\t\t\t\ttype: 'Point' as const,\n\t\t\t\t\t\t\t\t\tcoordinates: [s.lon, s.lat]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\tstopId: s.stopId,\n\t\t\t\t\t\t\t\t\tname: s.name,\n\t\t\t\t\t\t\t\t\ttime: s.arrival,\n\t\t\t\t\t\t\t\t\tcolor: getColor(l)[0]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}))\n\t\t\t\t\t\t: [];\n\n\t\t\t\t\treturn [...stops, ...intermediateStops];\n\t\t\t\t})\n\t\t};\n\t}\n\tconst geojson = $derived(stopsToGeoJSON(itinerary.legs));\n</script>\n\n<GeoJSON id=\"stops\" data={geojson}>\n\t<Layer\n\t\tid=\"stops\"\n\t\ttype=\"circle\"\n\t\tlayout={{}}\n\t\tfilter={['all']}\n\t\tpaint={{\n\t\t\t'circle-radius': 5,\n\t\t\t'circle-color': 'white',\n\t\t\t'circle-stroke-width': 4,\n\t\t\t'circle-stroke-color': ['get', 'color']\n\t\t}}\n\t\tonclick={(e) => {\n\t\t\tconst s = e.features?.[0];\n\t\t\tif (!s?.properties?.stopId) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log('Clicked Stop:', s.properties.name);\n\t\t\tonClickStop(s.properties.name, s.properties.stopId, new Date(s.properties.time));\n\t\t}}\n\t\tonmousemove={(_, map) => (map.getCanvas().style.cursor = 'pointer')}\n\t\tonmouseleave={(_, map) => (map.getCanvas().style.cursor = '')}\n\t/>\n\t<Layer\n\t\tid=\"intermediate-stops-name\"\n\t\ttype=\"symbol\"\n\t\tlayout={{\n\t\t\t'text-field': ['get', 'name'],\n\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t'text-size': 12,\n\t\t\t'text-offset': [0, 1],\n\t\t\t'text-anchor': 'top'\n\t\t}}\n\t\tfilter={true}\n\t\tpaint={{\n\t\t\t'text-halo-width': 2,\n\t\t\t// Use a darker hue of the line color for dark theme and a lighter hue for light theme\n\t\t\t'text-halo-color': [\n\t\t\t\t'interpolate-hcl',\n\t\t\t\t['linear'],\n\t\t\t\t0.6,\n\t\t\t\t0,\n\t\t\t\t['get', 'color'],\n\t\t\t\t1,\n\t\t\t\ttheme == 'dark' ? '#000' : '#fff'\n\t\t\t],\n\t\t\t'text-color': theme == 'dark' ? '#fff' : '#000'\n\t\t}}\n\t/>\n</GeoJSON>\n"
  },
  {
    "path": "ui/src/lib/map/style.ts",
    "content": "import type {\n\tHillshadeLayerSpecification,\n\tRasterDEMSourceSpecification,\n\tStyleSpecification\n} from 'maplibre-gl';\nconst colors = {\n\tlight: {\n\t\tbackground: '#f8f4f0',\n\n\t\twater: '#99ddff',\n\t\trail: '#a8a8a8',\n\t\tpedestrian: '#e8e7eb',\n\t\tferryRoute: 'rgba(102, 102, 255, 0.5)',\n\n\t\tsport: '#d0f4be',\n\t\tsportOutline: '#b3e998',\n\n\t\tbuilding: '#ded7d3',\n\t\tbuildingOutline: '#cfc8c4',\n\n\t\tlanduseComplex: '#f0e6d1',\n\t\tlanduseCommercial: 'hsla(0, 60%, 87%, 0.23)',\n\t\tlanduseIndustrial: '#e0e2eb',\n\t\tlanduseResidential: '#ece7e4',\n\t\tlanduseRetail: 'hsla(0, 60%, 87%, 0.23)',\n\t\tlanduseConstruction: '#aaa69d',\n\n\t\tlandusePark: '#b8ebad',\n\t\tlanduseNatureLight: '#ddecd7',\n\t\tlanduseNatureHeavy: '#a3e39c',\n\t\tlanduseCemetery: '#e0e4dd',\n\t\tlanduseBeach: '#fffcd3',\n\n\t\tindoorCorridor: '#fdfcfa',\n\t\tindoor: '#d4edff',\n\t\tindoorOutline: '#808080',\n\t\tindoorText: '#333333',\n\n\t\tpublicTransport: 'rgba(218,140,140,0.3)',\n\n\t\tfootway: '#fff',\n\t\tsteps: '#ff4524',\n\n\t\televatorOutline: '#808080',\n\t\televator: '#bcf1ba',\n\n\t\troadBackResidential: '#ffffff',\n\t\troadBackNonResidential: '#ffffff',\n\n\t\tmotorway: '#ffb366',\n\t\tmotorwayLink: '#f7e06e',\n\t\tprimarySecondary: '#fffbf8',\n\t\tlinkTertiary: '#ffffff',\n\t\tresidential: '#ffffff',\n\t\troad: '#ffffff',\n\n\t\ttownText: '#333333',\n\t\ttownTextHalo: 'white',\n\t\ttext: '#333333',\n\t\ttextHalo: 'white',\n\t\tcitiesText: '#111111',\n\t\tcitiesTextHalo: 'white',\n\n\t\tshield: 'shield'\n\t},\n\tdark: {\n\t\tbackground: '#292929',\n\n\t\twater: '#1f2830',\n\t\trail: '#808080',\n\t\tpedestrian: '#292929',\n\t\tferryRoute: 'rgba(58, 77, 139, 0.5)',\n\n\t\tsport: '#272525',\n\t\tsportOutline: '#272525',\n\n\t\tbuilding: '#1F1F1F',\n\t\tbuildingOutline: '#1A1A1A',\n\n\t\tlanduseComplex: '#292929',\n\t\tlanduseCommercial: '#292929',\n\t\tlanduseIndustrial: '#353538',\n\t\tlanduseResidential: '#292929',\n\t\tlanduseRetail: '#292929',\n\t\tlanduseConstruction: 'red',\n\n\t\tlandusePark: '#18221f',\n\t\tlanduseNatureLight: '#1e2322',\n\t\tlanduseNatureHeavy: '#1a2020',\n\t\tlanduseCemetery: '#202423',\n\t\tlanduseBeach: '#4c4b3e',\n\n\t\tindoorCorridor: '#494949',\n\t\tindoor: '#1a1a1a',\n\t\tindoorOutline: '#0d0d0d',\n\t\tindoorText: '#eeeeee',\n\n\t\tpublicTransport: 'rgba(89, 45, 45, 0.405)',\n\n\t\tfootway: '#3D3D3D',\n\t\tsteps: '#70504b',\n\n\t\televatorOutline: '#808080',\n\t\televator: '#3b423b',\n\n\t\troadBackResidential: '#414141',\n\t\troadBackNonResidential: '#414141',\n\n\t\tmotorway: '#414141',\n\t\tmotorwayLink: '#414141',\n\t\tprimarySecondary: '#414141',\n\t\tlinkTertiary: '#414141',\n\t\tresidential: '#414141',\n\t\troad: '#414141',\n\n\t\ttext: '#9a9a9a',\n\t\ttextHalo: '#151515',\n\t\ttownText: '#bebebe',\n\t\ttownTextHalo: '#1A1A1A',\n\t\tcitiesText: '#bebebe',\n\t\tcitiesTextHalo: '#1A1A1A',\n\n\t\tshield: 'shield-dark'\n\t}\n};\n\nfunction getUrlBase(url: string): string {\n\tconst { origin, pathname } = new URL(url);\n\treturn origin + pathname.slice(0, pathname.lastIndexOf('/') + 1);\n}\n\n// this doesn't escape {}-parameters\nfunction getAbsoluteUrl(base: string, relative: string): string {\n\treturn getUrlBase(base) + relative;\n}\n\nexport const getStyle = (\n\ttheme: 'light' | 'dark',\n\tlevel: number,\n\tstaticBaseUrl: string,\n\tapiBaseUrl: string,\n\twithHillshades: boolean\n): StyleSpecification => {\n\tconst c = colors[theme];\n\tconst hillshadeSources: StyleSpecification['sources'] = withHillshades\n\t\t? {\n\t\t\t\thillshadeSource: {\n\t\t\t\t\ttype: 'raster-dem',\n\t\t\t\t\turl: 'https://tiles.mapterhorn.com/tilejson.json'\n\t\t\t\t} satisfies RasterDEMSourceSpecification\n\t\t\t}\n\t\t: {};\n\tconst hillshadeLayers: HillshadeLayerSpecification[] = withHillshades\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tid: 'hillshade',\n\t\t\t\t\ttype: 'hillshade',\n\t\t\t\t\tsource: 'hillshadeSource',\n\t\t\t\t\tpaint: {\n\t\t\t\t\t\t'hillshade-exaggeration': 0.33\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t: [];\n\treturn {\n\t\tversion: 8,\n\t\tsources: {\n\t\t\tosm: {\n\t\t\t\ttype: 'vector',\n\t\t\t\ttiles: [getAbsoluteUrl(apiBaseUrl, 'tiles/{z}/{x}/{y}.mvt')],\n\t\t\t\tmaxzoom: 20,\n\t\t\t\tattribution: ''\n\t\t\t},\n\t\t\t...hillshadeSources\n\t\t},\n\t\tglyphs: getAbsoluteUrl(apiBaseUrl, 'tiles/glyphs/{fontstack}/{range}.pbf'),\n\t\tsprite: getAbsoluteUrl(staticBaseUrl, 'sprite_sdf'),\n\t\tlayers: [\n\t\t\t{\n\t\t\t\tid: 'background',\n\t\t\t\ttype: 'background',\n\t\t\t\tpaint: { 'background-color': c.background }\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'coastline',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'coastline',\n\t\t\t\tpaint: { 'fill-color': c.water }\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'landuse_park',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'landuse',\n\t\t\t\tfilter: ['==', ['get', 'landuse'], 'park'],\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': c.landusePark\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'landuse',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'landuse',\n\t\t\t\tfilter: ['!in', 'landuse', 'park', 'public_transport'],\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': [\n\t\t\t\t\t\t'match',\n\t\t\t\t\t\t['get', 'landuse'],\n\t\t\t\t\t\t'complex',\n\t\t\t\t\t\tc.landuseComplex,\n\t\t\t\t\t\t'commercial',\n\t\t\t\t\t\tc.landuseCommercial,\n\t\t\t\t\t\t'industrial',\n\t\t\t\t\t\tc.landuseIndustrial,\n\t\t\t\t\t\t'residential',\n\t\t\t\t\t\tc.landuseResidential,\n\t\t\t\t\t\t'retail',\n\t\t\t\t\t\tc.landuseRetail,\n\t\t\t\t\t\t'construction',\n\t\t\t\t\t\tc.landuseConstruction,\n\n\t\t\t\t\t\t'nature_light',\n\t\t\t\t\t\tc.landuseNatureLight,\n\t\t\t\t\t\t'nature_heavy',\n\t\t\t\t\t\tc.landuseNatureHeavy,\n\t\t\t\t\t\t'cemetery',\n\t\t\t\t\t\tc.landuseCemetery,\n\t\t\t\t\t\t'beach',\n\t\t\t\t\t\tc.landuseBeach,\n\n\t\t\t\t\t\t'magenta'\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'water',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'water',\n\t\t\t\tpaint: { 'fill-color': c.water }\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'sport',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'sport',\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': c.sport,\n\t\t\t\t\t'fill-outline-color': c.sportOutline\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'pedestrian',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'pedestrian',\n\t\t\t\tpaint: { 'fill-color': c.pedestrian }\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'waterway',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'waterway',\n\t\t\t\tpaint: { 'line-color': c.water }\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'building',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'building',\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': c.building,\n\t\t\t\t\t'fill-outline-color': c.buildingOutline,\n\t\t\t\t\t'fill-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 16, 0.8]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor-corridor',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tfilter: ['all', ['==', 'indoor', 'corridor'], ['==', 'level', level]],\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': c.indoorCorridor,\n\t\t\t\t\t'fill-opacity': 0.8\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tfilter: ['all', ['!in', 'indoor', 'corridor', 'wall', 'elevator'], ['==', 'level', level]],\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': c.indoor,\n\t\t\t\t\t'fill-opacity': 0.8\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor-outline',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tfilter: ['all', ['!in', 'indoor', 'corridor', 'wall', 'elevator'], ['==', 'level', level]],\n\t\t\t\tminzoom: 18,\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.indoorOutline,\n\t\t\t\t\t'line-width': 2\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor-names',\n\t\t\t\ttype: 'symbol',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: ['any', ['!has', 'level'], ['==', 'level', level]],\n\t\t\t\tlayout: {\n\t\t\t\t\t'symbol-placement': 'point',\n\t\t\t\t\t'text-field': ['get', 'name'],\n\t\t\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t\t\t'text-size': 12\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'text-color': c.indoorText\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'landuse-public-transport',\n\t\t\t\ttype: 'fill',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'landuse',\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'landuse', 'public_transport'],\n\t\t\t\t\t['any', ['!has', 'level'], ['==', 'level', level]]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'fill-color': c.publicTransport\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'footway',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['in', 'highway', 'footway', 'track', 'cycleway', 'path', 'unclassified', 'service'],\n\t\t\t\t\tlevel === 0 ? ['any', ['!has', 'level'], ['==', 'level', level]] : ['==', 'level', level]\n\t\t\t\t],\n\t\t\t\tlayout: {\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t},\n\t\t\t\tminzoom: 14,\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-dasharray': [0.75, 1.5],\n\t\t\t\t\t'line-color': c.footway,\n\t\t\t\t\t'line-opacity': 0.5,\n\t\t\t\t\t'line-width': [\n\t\t\t\t\t\t'let',\n\t\t\t\t\t\t'base',\n\t\t\t\t\t\t0.4,\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t\t['linear'],\n\t\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t\t5,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 0.1], 1],\n\t\t\t\t\t\t\t9,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 0.4], 1],\n\t\t\t\t\t\t\t12,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 1], 1],\n\t\t\t\t\t\t\t16,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 4], 1],\n\t\t\t\t\t\t\t20,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 8], 1]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stairs-ground',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'highway', 'steps'],\n\t\t\t\t\tlevel === 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t'any',\n\t\t\t\t\t\t\t\t['!has', 'from_level'],\n\t\t\t\t\t\t\t\t['any', ['==', 'from_level', level], ['==', 'to_level', level]]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: ['any', ['==', 'from_level', level], ['==', 'to_level', level]]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': '#ddddddff',\n\t\t\t\t\t'line-width': [\n\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t['exponential', 2],\n\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t['*', 4, ['^', 2, -6]],\n\t\t\t\t\t\t24,\n\t\t\t\t\t\t['*', 4, ['^', 2, 8]]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stairs-steps',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'highway', 'steps'],\n\t\t\t\t\tlevel === 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t'any',\n\t\t\t\t\t\t\t\t['!has', 'from_level'],\n\t\t\t\t\t\t\t\t['any', ['==', 'from_level', level], ['==', 'to_level', level]]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: ['any', ['==', 'from_level', level], ['==', 'to_level', level]]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': '#bfbfbf',\n\t\t\t\t\t'line-dasharray': ['literal', [0.01, 0.1]],\n\t\t\t\t\t'line-width': [\n\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t['exponential', 2],\n\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t['*', 4, ['^', 2, -6]],\n\t\t\t\t\t\t24,\n\t\t\t\t\t\t['*', 4, ['^', 2, 8]]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stairs-rail',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'highway', 'steps'],\n\t\t\t\t\tlevel === 0\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t'any',\n\t\t\t\t\t\t\t\t['!has', 'from_level'],\n\t\t\t\t\t\t\t\t['any', ['==', 'from_level', level], ['==', 'to_level', level]]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: ['any', ['==', 'from_level', level], ['==', 'to_level', level]]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': '#808080',\n\t\t\t\t\t'line-width': [\n\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t['exponential', 2],\n\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t['*', 0.25, ['^', 2, -6]],\n\t\t\t\t\t\t24,\n\t\t\t\t\t\t['*', 0.25, ['^', 2, 8]]\n\t\t\t\t\t],\n\t\t\t\t\t'line-gap-width': [\n\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t['exponential', 2],\n\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t10,\n\t\t\t\t\t\t['*', 4, ['^', 2, -6]],\n\t\t\t\t\t\t24,\n\t\t\t\t\t\t['*', 4, ['^', 2, 8]]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor-elevator-outline',\n\t\t\t\ttype: 'circle',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'indoor', 'elevator'],\n\t\t\t\t\t['<=', 'from_level', level],\n\t\t\t\t\t['>=', 'to_level', level]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'circle-color': c.elevatorOutline,\n\t\t\t\t\t'circle-radius': 16\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor-elevator',\n\t\t\t\ttype: 'circle',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'indoor', 'elevator'],\n\t\t\t\t\t['<=', 'from_level', level],\n\t\t\t\t\t['>=', 'to_level', level]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'circle-color': c.elevator,\n\t\t\t\t\t'circle-radius': 14\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'indoor-elevator-icon',\n\t\t\t\ttype: 'symbol',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'indoor',\n\t\t\t\tminzoom: 18,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'indoor', 'elevator'],\n\t\t\t\t\t['<=', 'from_level', level],\n\t\t\t\t\t['>=', 'to_level', level]\n\t\t\t\t],\n\t\t\t\tlayout: {\n\t\t\t\t\t'icon-image': 'elevator',\n\t\t\t\t\t'icon-size': 0.9\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'road_back_residential',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tfilter: ['==', 'highway', 'residential'],\n\t\t\t\tlayout: {\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.roadBackResidential,\n\t\t\t\t\t'line-width': ['interpolate', ['linear'], ['zoom'], 5, 0, 9, 0.5, 12, 1, 16, 4, 20, 20],\n\t\t\t\t\t'line-opacity': ['interpolate', ['linear'], ['zoom'], 12, 0.4, 15, 1]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'road_back_non_residential',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tfilter: [\n\t\t\t\t\t'!in',\n\t\t\t\t\t'highway',\n\t\t\t\t\t'footway',\n\t\t\t\t\t'track',\n\t\t\t\t\t'steps',\n\t\t\t\t\t'cycleway',\n\t\t\t\t\t'path',\n\t\t\t\t\t'unclassified',\n\t\t\t\t\t'residential',\n\t\t\t\t\t'service'\n\t\t\t\t],\n\t\t\t\tlayout: {\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.roadBackNonResidential,\n\t\t\t\t\t'line-width': [\n\t\t\t\t\t\t'let',\n\t\t\t\t\t\t'base',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'match',\n\t\t\t\t\t\t\t['get', 'highway'],\n\t\t\t\t\t\t\t'motorway',\n\t\t\t\t\t\t\t4,\n\t\t\t\t\t\t\t['trunk', 'motorway_link'],\n\t\t\t\t\t\t\t3.5,\n\t\t\t\t\t\t\t['primary', 'secondary', 'aeroway', 'trunk_link'],\n\t\t\t\t\t\t\t3,\n\t\t\t\t\t\t\t['primary_link', 'secondary_link', 'tertiary', 'tertiary_link'],\n\t\t\t\t\t\t\t1.75,\n\t\t\t\t\t\t\t0.0\n\t\t\t\t\t\t],\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t\t['linear'],\n\t\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t\t5,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 0.1], 1],\n\t\t\t\t\t\t\t9,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 0.4], 1],\n\t\t\t\t\t\t\t12,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 1], 1],\n\t\t\t\t\t\t\t16,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 4], 1],\n\t\t\t\t\t\t\t20,\n\t\t\t\t\t\t\t['+', ['*', ['var', 'base'], 8], 1]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'road',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tlayout: {\n\t\t\t\t\t'line-cap': 'round'\n\t\t\t\t},\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['has', 'ref'],\n\t\t\t\t\t[\n\t\t\t\t\t\t'any',\n\t\t\t\t\t\t['==', ['get', 'highway'], 'motorway'],\n\t\t\t\t\t\t['==', ['get', 'highway'], 'trunk'],\n\t\t\t\t\t\t['==', ['get', 'highway'], 'secondary'],\n\t\t\t\t\t\t['>', ['zoom'], 11]\n\t\t\t\t\t]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': [\n\t\t\t\t\t\t'match',\n\t\t\t\t\t\t['get', 'highway'],\n\t\t\t\t\t\t'motorway',\n\t\t\t\t\t\tc.motorway,\n\t\t\t\t\t\t['trunk', 'motorway_link'],\n\t\t\t\t\t\tc.motorwayLink,\n\t\t\t\t\t\t['primary', 'secondary', 'aeroway', 'trunk_link'],\n\t\t\t\t\t\tc.primarySecondary,\n\t\t\t\t\t\t['primary_link', 'secondary_link', 'tertiary', 'tertiary_link'],\n\t\t\t\t\t\tc.linkTertiary,\n\t\t\t\t\t\t'residential',\n\t\t\t\t\t\tc.residential,\n\t\t\t\t\t\tc.road\n\t\t\t\t\t],\n\t\t\t\t\t'line-width': [\n\t\t\t\t\t\t'let',\n\t\t\t\t\t\t'base',\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'match',\n\t\t\t\t\t\t\t['get', 'highway'],\n\t\t\t\t\t\t\t'motorway',\n\t\t\t\t\t\t\t3.5,\n\t\t\t\t\t\t\t['trunk', 'motorway_link'],\n\t\t\t\t\t\t\t3,\n\t\t\t\t\t\t\t['primary', 'secondary', 'aeroway', 'trunk_link'],\n\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t['primary_link', 'secondary_link', 'tertiary', 'tertiary_link'],\n\t\t\t\t\t\t\t1.75,\n\t\t\t\t\t\t\t'residential',\n\t\t\t\t\t\t\t1.5,\n\t\t\t\t\t\t\t0.75\n\t\t\t\t\t\t],\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'interpolate',\n\t\t\t\t\t\t\t['linear'],\n\t\t\t\t\t\t\t['zoom'],\n\t\t\t\t\t\t\t5,\n\t\t\t\t\t\t\t['*', ['var', 'base'], 0.5],\n\t\t\t\t\t\t\t9,\n\t\t\t\t\t\t\t['*', ['var', 'base'], 1],\n\t\t\t\t\t\t\t12,\n\t\t\t\t\t\t\t['*', ['var', 'base'], 2],\n\t\t\t\t\t\t\t16,\n\t\t\t\t\t\t\t['*', ['var', 'base'], 2.5],\n\t\t\t\t\t\t\t20,\n\t\t\t\t\t\t\t['*', ['var', 'base'], 3]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t...hillshadeLayers,\n\t\t\t{\n\t\t\t\tid: 'ferry_routes',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'ferry',\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-width': 1.5,\n\t\t\t\t\t'line-dasharray': [2, 3],\n\t\t\t\t\t'line-color': c.ferryRoute\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'rail_detail',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'rail',\n\t\t\t\tfilter: ['==', 'rail', 'detail'],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.rail\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'rail_secondary',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'rail',\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'rail', 'secondary'],\n\t\t\t\t\t['any', ['!has', 'level'], ['==', 'level', level]]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.rail,\n\t\t\t\t\t'line-width': 1.15\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'rail_primary',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'rail',\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['==', 'rail', 'primary'],\n\t\t\t\t\t['any', ['!has', 'level'], ['==', 'level', level]]\n\t\t\t\t],\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.rail,\n\t\t\t\t\t'line-width': 1.3\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'aerialway',\n\t\t\t\ttype: 'line',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'aerialway',\n\t\t\t\tpaint: {\n\t\t\t\t\t'line-color': c.rail,\n\t\t\t\t\t'line-dasharray': [10, 2]\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'road-ref-shield',\n\t\t\t\ttype: 'symbol',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tminzoom: 6,\n\t\t\t\tfilter: [\n\t\t\t\t\t'all',\n\t\t\t\t\t['has', 'ref'],\n\t\t\t\t\t[\n\t\t\t\t\t\t'any',\n\t\t\t\t\t\t['==', ['get', 'highway'], 'motorway'],\n\t\t\t\t\t\t['==', ['get', 'highway'], 'trunk'],\n\t\t\t\t\t\t['==', ['get', 'highway'], 'secondary'],\n\t\t\t\t\t\t['>', ['zoom'], 11]\n\t\t\t\t\t]\n\t\t\t\t],\n\t\t\t\tlayout: {\n\t\t\t\t\t'symbol-placement': 'line',\n\t\t\t\t\t'text-field': ['get', 'ref'],\n\t\t\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t\t\t'text-size': ['case', ['==', ['get', 'highway'], 'motorway'], 11, 10],\n\t\t\t\t\t'text-justify': 'center',\n\t\t\t\t\t'text-rotation-alignment': 'viewport',\n\t\t\t\t\t'text-pitch-alignment': 'viewport',\n\t\t\t\t\t'icon-image': c.shield,\n\t\t\t\t\t'icon-text-fit': 'both',\n\t\t\t\t\t'icon-text-fit-padding': [0.5, 4, 0.5, 4],\n\t\t\t\t\t'icon-rotation-alignment': 'viewport',\n\t\t\t\t\t'icon-pitch-alignment': 'viewport'\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'text-color': c.text\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'road-name-text',\n\t\t\t\ttype: 'symbol',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'road',\n\t\t\t\tminzoom: 14,\n\t\t\t\tlayout: {\n\t\t\t\t\t'symbol-placement': 'line',\n\t\t\t\t\t'text-field': ['get', 'name'],\n\t\t\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t\t\t'text-size': 9\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'text-halo-width': 11,\n\t\t\t\t\t'text-halo-color': c.textHalo,\n\t\t\t\t\t'text-color': c.text\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'towns',\n\t\t\t\ttype: 'symbol',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'cities',\n\t\t\t\tfilter: ['!=', ['get', 'place'], 'city'],\n\t\t\t\tlayout: {\n\t\t\t\t\t// \"symbol-sort-key\": [\"get\", \"population\"],\n\t\t\t\t\t'text-field': ['get', 'name'],\n\t\t\t\t\t'text-font': ['Noto Sans Regular'],\n\t\t\t\t\t'text-size': 12\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'text-halo-width': 1,\n\t\t\t\t\t'text-halo-color': c.textHalo,\n\t\t\t\t\t'text-color': c.text\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'cities',\n\t\t\t\ttype: 'symbol',\n\t\t\t\tsource: 'osm',\n\t\t\t\t'source-layer': 'cities',\n\t\t\t\tfilter: ['==', ['get', 'place'], 'city'],\n\t\t\t\tlayout: {\n\t\t\t\t\t'symbol-sort-key': ['-', ['coalesce', ['get', 'population'], 0]],\n\t\t\t\t\t'text-field': ['get', 'name'],\n\t\t\t\t\t'text-font': ['Noto Sans Bold'],\n\t\t\t\t\t'text-size': ['interpolate', ['linear'], ['zoom'], 6, 12, 9, 16]\n\t\t\t\t},\n\t\t\t\tpaint: {\n\t\t\t\t\t'text-halo-width': 2,\n\t\t\t\t\t'text-halo-color': 'white',\n\t\t\t\t\t'text-color': 'hsl(0, 0%, 20%)'\n\t\t\t\t}\n\t\t\t\t// }, {\n\t\t\t\t//    \"id\": \"tiles_debug_info_bound\",\n\t\t\t\t//    \"type\": \"line\",\n\t\t\t\t//    \"source\": \"osm\",\n\t\t\t\t//    \"source-layer\": \"tiles_debug_info\",\n\t\t\t\t//    \"paint\": {\n\t\t\t\t//      \"line-color\": \"magenta\",\n\t\t\t\t//      \"line-width\": 1\n\t\t\t\t//    }\n\t\t\t\t// }, {\n\t\t\t\t//     \"id\": \"tiles_debug_info_name\",\n\t\t\t\t//     \"type\": \"symbol\",\n\t\t\t\t//     \"source\": \"osm\",\n\t\t\t\t//     \"source-layer\": \"tiles_debug_info\",\n\t\t\t\t//     \"layout\": {\n\t\t\t\t//       \"text-field\": [\"get\", \"tile_id\"],\n\t\t\t\t//       \"text-font\": [\"Noto Sans Bold\"],\n\t\t\t\t//       \"text-size\": 16,\n\t\t\t\t//     },\n\t\t\t\t//     \"paint\": {\n\t\t\t\t//       \"text-halo-width\": 2,\n\t\t\t\t//       \"text-halo-color\": \"white\",\n\t\t\t\t//       \"text-color\": \"hsl(0, 0%, 20%)\"\n\t\t\t\t//     }\n\t\t\t}\n\t\t]\n\t};\n};\n"
  },
  {
    "path": "ui/src/lib/modeStyle.ts",
    "content": "import type { Mode, Rental } from '@motis-project/motis-client';\n\nexport type Colorable = { routeColor?: string; routeTextColor?: string; mode: Mode };\n\nexport type TripInfo = { tripId?: string; displayName?: string };\n\nexport type RentalInfo = { rental?: Rental };\n\nexport type LegLike = Colorable & TripInfo & RentalInfo;\n\nexport const getModeStyle = (l: LegLike): [string, string, string] => {\n\tswitch (l.mode) {\n\t\tcase 'WALK':\n\t\t\treturn ['walk', 'hsl(var(--foreground) / 1)', 'hsl(var(--background) / 1)'];\n\t\tcase 'BIKE':\n\t\t\treturn ['bike', 'hsl(var(--foreground) / 1)', 'hsl(var(--background) / 1)'];\n\n\t\tcase 'RENTAL':\n\t\t\tswitch (l.rental?.formFactor) {\n\t\t\t\tcase 'BICYCLE':\n\t\t\t\t\treturn ['bike', '#075985', 'white'];\n\t\t\t\tcase 'CARGO_BICYCLE':\n\t\t\t\t\treturn ['cargo_bike', '#075985', 'white'];\n\t\t\t\tcase 'CAR':\n\t\t\t\t\treturn ['car', '#4c4947', 'white'];\n\t\t\t\tcase 'MOPED':\n\t\t\t\t\treturn ['moped', '#075985', 'white'];\n\t\t\t\tcase 'SCOOTER_SEATED':\n\t\t\t\t\treturn ['seated_scooter', '#075985', 'white'];\n\t\t\t\tcase 'SCOOTER_STANDING':\n\t\t\t\t\treturn ['scooter', '#075985', 'white'];\n\t\t\t\tcase 'OTHER':\n\t\t\t\tdefault:\n\t\t\t\t\treturn ['other', '#075985', 'white'];\n\t\t\t}\n\n\t\tcase 'RIDE_SHARING':\n\t\t\treturn ['car', '#217edb', 'white'];\n\n\t\tcase 'CAR':\n\t\tcase 'CAR_PARKING':\n\t\t\treturn ['car', '#4c4947', 'white'];\n\n\t\tcase 'FLEX':\n\t\tcase 'ODM':\n\t\t\treturn ['taxi', '#fdb813', 'white'];\n\n\t\tcase 'TRANSIT':\n\t\tcase 'BUS':\n\t\t\treturn ['bus', '#ff9800', 'white'];\n\t\tcase 'COACH':\n\t\t\treturn ['bus', '#9ccc65', 'black'];\n\n\t\tcase 'TRAM':\n\t\t\treturn ['tram', '#ebe717', 'white'];\n\n\t\tcase 'SUBURBAN':\n\t\t\treturn ['sbahn', '#4caf50', 'white'];\n\n\t\tcase 'SUBWAY':\n\t\t\treturn ['ubahn', '#3f51b5', 'white'];\n\n\t\tcase 'FERRY':\n\t\t\treturn ['ship', '#00acc1', 'white'];\n\n\t\tcase 'AIRPLANE':\n\t\t\treturn ['plane', '#90a4ae', 'white'];\n\n\t\tcase 'HIGHSPEED_RAIL':\n\t\t\treturn ['train', '#9c27b0', 'white'];\n\n\t\tcase 'LONG_DISTANCE':\n\t\t\treturn ['train', '#e91e63', 'white'];\n\n\t\tcase 'NIGHT_RAIL':\n\t\t\treturn ['train', '#1a237e', 'white'];\n\n\t\tcase 'REGIONAL_FAST_RAIL':\n\t\tcase 'REGIONAL_RAIL':\n\t\tcase 'RAIL':\n\t\t\treturn ['train', '#f44336', 'white'];\n\n\t\tcase 'FUNICULAR':\n\t\t\treturn ['funicular', '#795548', 'white'];\n\n\t\tcase 'CABLE_CAR':\n\t\t\treturn ['tram', '#795548', 'white'];\n\n\t\tcase 'AERIAL_LIFT':\n\t\t\treturn ['aerial_lift', '#795548', 'white'];\n\t}\n\n\treturn ['train', '#000000', 'white'];\n};\n\nexport const getColor = (l: Colorable): [string, string] => {\n\tconst [_, defaultColor, defaultTextColor] = getModeStyle(l);\n\tif (!l.routeColor) {\n\t\treturn [defaultColor, defaultTextColor];\n\t}\n\treturn [`#${l.routeColor}`, `#${l.routeTextColor}`];\n};\n\nexport const routeBorderColor = (l: Colorable) => {\n\treturn `border-color: ${getColor(l)[0]}`;\n};\n\nexport const routeColor = (l: Colorable) => {\n\tconst [color, textColor] = getColor(l);\n\treturn `background-color: ${color}; color: ${textColor}; fill: ${textColor}`;\n};\n"
  },
  {
    "path": "ui/src/lib/preprocessItinerary.ts",
    "content": "import type {\n\tItinerary,\n\tPlace,\n\tPlanResponse,\n\tError as ApiError\n} from '@motis-project/motis-client';\nimport type { Location } from '$lib/Location';\nimport polyline from '@mapbox/polyline';\nimport type { RequestResult } from '@hey-api/client-fetch';\n\nexport const joinInterlinedLegs = (it: Itinerary) => {\n\tconst joinedLegs = [];\n\tfor (let i = 0; i < it.legs.length; i++) {\n\t\tif (it.legs[i].interlineWithPreviousLeg) {\n\t\t\tconst pred = joinedLegs[joinedLegs.length - 1];\n\t\t\tconst curr = it.legs[i];\n\t\t\tpred.intermediateStops!.push({ ...pred.to, switchTo: curr } as Place);\n\t\t\tpred.to = curr.to;\n\t\t\tpred.duration += curr.duration;\n\t\t\tpred.endTime = curr.endTime;\n\t\t\tpred.scheduledEndTime = curr.scheduledEndTime;\n\t\t\tpred.realTime ||= curr.realTime;\n\t\t\tpred.intermediateStops!.push(...curr.intermediateStops!);\n\t\t\tpred.legGeometry = {\n\t\t\t\tpoints: polyline.encode(\n\t\t\t\t\t[\n\t\t\t\t\t\t...polyline.decode(pred.legGeometry.points, pred.legGeometry.precision),\n\t\t\t\t\t\t...polyline.decode(curr.legGeometry.points, curr.legGeometry.precision)\n\t\t\t\t\t],\n\t\t\t\t\tpred.legGeometry.precision\n\t\t\t\t),\n\t\t\t\tprecision: pred.legGeometry.precision,\n\t\t\t\tlength: pred.legGeometry.length + curr.legGeometry.length\n\t\t\t};\n\t\t} else {\n\t\t\tjoinedLegs.push(it.legs[i]);\n\t\t}\n\t}\n\tit.legs = joinedLegs;\n};\n\nexport const preprocessItinerary = (from: Location, to: Location) => {\n\tconst updateItinerary = (it: Itinerary) => {\n\t\tif (it.legs[0].from.name === 'START') {\n\t\t\tit.legs[0].from.name = from.label!;\n\t\t}\n\t\tif (it.legs[it.legs.length - 1].to.name === 'END') {\n\t\t\tit.legs[it.legs.length - 1].to.name = to.label!;\n\t\t}\n\t\tjoinInterlinedLegs(it);\n\t};\n\n\treturn (r: Awaited<RequestResult<PlanResponse, ApiError, false>>): PlanResponse => {\n\t\tif (r.error) throw { error: r.error.error, status: r.response?.status };\n\t\tr.data.itineraries.forEach(updateItinerary);\n\t\tr.data.direct.forEach(updateItinerary);\n\n\t\treturn r.data;\n\t};\n};\n"
  },
  {
    "path": "ui/src/lib/toDateTime.ts",
    "content": "import { language } from './i18n/translation';\n\nexport const formatTime = (d: Date, timeZone: string | undefined): string => {\n\treturn d.toLocaleTimeString(language, {\n\t\thour: 'numeric',\n\t\tminute: 'numeric',\n\t\ttimeZone,\n\t\thour12: false\n\t});\n};\n\nexport const formatDate = (d: Date, timeZone: string | undefined): string => {\n\treturn d.toLocaleDateString(language, {\n\t\tday: 'numeric',\n\t\tmonth: 'numeric',\n\t\tyear: 'numeric',\n\t\ttimeZone\n\t});\n};\n\nexport const formatDateTime = (d: Date, timeZone: string | undefined): string => {\n\treturn d.toLocaleDateString(language, {\n\t\tday: 'numeric',\n\t\tmonth: 'numeric',\n\t\tyear: 'numeric',\n\t\thour: 'numeric',\n\t\tminute: 'numeric',\n\t\ttimeZone\n\t});\n};\n\nexport const getTz = (d: Date, timeZone: string | undefined): string | undefined => {\n\tconst timeZoneOffset = new Intl.DateTimeFormat(language, {\n\t\ttimeZone,\n\t\ttimeZoneName: 'shortOffset'\n\t})\n\t\t.formatToParts(d)\n\t\t.find((part) => part.type === 'timeZoneName')!.value;\n\tconst isSameAsBrowserTimezone =\n\t\tnew Intl.DateTimeFormat(language, { timeZoneName: 'shortOffset' })\n\t\t\t.formatToParts(d)\n\t\t\t.find((part) => part.type === 'timeZoneName')!.value == timeZoneOffset;\n\treturn isSameAsBrowserTimezone ? undefined : timeZoneOffset;\n};\n"
  },
  {
    "path": "ui/src/lib/tripsWorker.ts",
    "content": "/// <reference lib=\"webworker\" />trips\nimport polyline from '@mapbox/polyline';\nimport {\n\tclient,\n\ttrips,\n\ttype Mode,\n\ttype TripsData,\n\ttype TripSegment\n} from '@motis-project/motis-client';\nimport type { Trip, TransferData, MetaData } from './types';\nimport type { QuerySerializerOptions } from '@hey-api/client-fetch';\nimport { getDelayColor, hexToRgb } from './Color';\nimport { getModeStyle, getColor } from './modeStyle';\n\n//MATH\nconst R = 6371;\nconst TO_RAD = Math.PI / 180;\nconst TO_DEG = 180 / Math.PI;\n\nconst getSpacialData = (\n\tpath: Float64Array,\n\ti: number,\n\tsegmentDistances: Float64Array,\n\theadings: Float32Array\n) => {\n\tconst i2 = i * 2;\n\tconst i1 = i2 - 2;\n\tconst lon2 = path[i2] * TO_RAD;\n\tconst lat2 = path[i2 + 1] * TO_RAD;\n\tconst lon1 = path[i1] * TO_RAD;\n\tconst lat1 = path[i1 + 1] * TO_RAD;\n\n\tconst dLon = lon2 - lon1;\n\tconst dLat = lat2 - lat1;\n\n\tconst cosLat1 = Math.cos(lat1);\n\tconst cosLat2 = Math.cos(lat2);\n\tconst sinLat1 = Math.sin(lat1);\n\tconst sinLat2 = Math.sin(lat2);\n\tconst cosDLon = Math.cos(dLon);\n\n\t// Haversine Distance\n\tconst a = Math.sin(dLat / 2) ** 2 + cosLat1 * cosLat2 * Math.sin(dLon / 2) ** 2;\n\tconst dist = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * R;\n\tsegmentDistances[i - 1] = dist;\n\n\t// Bearing\n\tconst y = Math.sin(dLon) * cosLat2;\n\tconst x = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosDLon;\n\theadings[i - 1] = (-(Math.atan2(y, x) * TO_DEG + 360) % 360) + 90;\n};\n\n//PROCESSING\nlet status: number;\nconst tripsMap = new Map<string, Trip>();\nconst metaDataMap = new Map<string, MetaData>();\nlet metadata: MetaData[] = [];\nconst fetchData = async (query: TripsData) => {\n\tconst { data, response } = await trips(query);\n\tstatus = response.status;\n\tif (!data) return;\n\n\ttripsMap.clear();\n\tmetaDataMap.clear();\n\n\tconst tripBuilders = new Map<\n\t\tstring,\n\t\t{\n\t\t\tpaths: Float64Array[];\n\t\t\ttimestamps: Float64Array[];\n\t\t\theadings: Float32Array[];\n\t\t\trealtime: boolean;\n\t\t\tmode: Mode;\n\t\t\trouteColor?: string;\n\t\t\tdepartureDelay: number;\n\t\t\tarrivalDelay: number;\n\t\t}\n\t>();\n\n\tfor (const d of data) {\n\t\tconst id = d.trips[0].tripId;\n\t\tconst precision =\n\t\t\t'precision' in query.query && typeof query.query.precision === 'number'\n\t\t\t\t? query.query.precision\n\t\t\t\t: 5;\n\t\tconst processed = processSegment(d, precision);\n\n\t\tif (!tripBuilders.has(id)) {\n\t\t\ttripBuilders.set(id, {\n\t\t\t\tpaths: [processed.path],\n\t\t\t\ttimestamps: [processed.timestamps],\n\t\t\t\theadings: [processed.headings],\n\t\t\t\trealtime: processed.realtime,\n\t\t\t\tmode: processed.mode,\n\t\t\t\trouteColor: processed.routeColor,\n\t\t\t\tdepartureDelay: processed.departureDelay,\n\t\t\t\tarrivalDelay: processed.arrivalDelay\n\t\t\t});\n\t\t} else {\n\t\t\tconst b = tripBuilders.get(id)!;\n\t\t\tb.paths.push(processed.path);\n\t\t\tb.timestamps.push(processed.timestamps);\n\t\t\tb.headings.push(processed.headings);\n\t\t}\n\n\t\tmetaDataMap.set(id, {\n\t\t\tid,\n\t\t\tdisplayName: d.trips[0].displayName,\n\t\t\ttz: d.from.tz,\n\t\t\tfrom: d.from.name,\n\t\t\tto: d.to.name,\n\t\t\trealtime: d.realTime,\n\t\t\tarrival: d.arrival,\n\t\t\tdeparture: d.departure,\n\t\t\tscheduledArrival: d.scheduledArrival,\n\t\t\tscheduledDeparture: d.scheduledDeparture,\n\t\t\tdepartureDelay: processed.departureDelay,\n\t\t\tarrivalDelay: processed.arrivalDelay\n\t\t});\n\t}\n\n\ttripBuilders.forEach((b, id) => {\n\t\tlet pathLen = 0;\n\t\tlet tsLen = 0;\n\t\tlet hdLen = 0;\n\n\t\tfor (let i = 0; i < b.paths.length; i++) {\n\t\t\tpathLen += b.paths[i].length;\n\t\t\ttsLen += b.timestamps[i].length;\n\t\t\thdLen += b.headings[i].length;\n\t\t}\n\n\t\tconst path = new Float64Array(pathLen);\n\t\tconst timestamps = new Float64Array(tsLen);\n\t\tconst headings = new Float32Array(hdLen);\n\n\t\tlet pOff = 0,\n\t\t\ttOff = 0,\n\t\t\thOff = 0;\n\t\tfor (let i = 0; i < b.paths.length; i++) {\n\t\t\tpath.set(b.paths[i], pOff);\n\t\t\ttimestamps.set(b.timestamps[i], tOff);\n\t\t\theadings.set(b.headings[i], hOff);\n\n\t\t\tpOff += b.paths[i].length;\n\t\t\ttOff += b.timestamps[i].length;\n\t\t\thOff += b.headings[i].length;\n\t\t}\n\n\t\ttripsMap.set(id, {\n\t\t\trealtime: b.realtime,\n\t\t\tmode: b.mode,\n\t\t\trouteColor: b.routeColor,\n\t\t\tpath,\n\t\t\ttimestamps,\n\t\t\theadings,\n\t\t\tcurrentIndx: 0,\n\t\t\tdepartureDelay: b.departureDelay,\n\t\t\tarrivalDelay: b.arrivalDelay\n\t\t});\n\t});\n\tmetadata = Array.from(metaDataMap.values());\n};\nconst processSegment = (s: TripSegment, precision: number): Trip => {\n\tconst departure = new Date(s.departure).getTime();\n\tconst arrival = new Date(s.arrival).getTime();\n\tconst totalDuration = arrival - departure;\n\n\tconst decoded = polyline.decode(s.polyline, precision);\n\tconst count = decoded.length;\n\n\tconst path = new Float64Array(count * 2);\n\tconst timestamps = new Float64Array(count);\n\tconst headings = new Float32Array(count);\n\tconst segmentDistances = new Float64Array(count);\n\n\tlet totalDistance = 0;\n\n\tfor (let i = 0; i < count; i++) {\n\t\tconst p = decoded[i];\n\t\tconst lon = p[1];\n\t\tconst lat = p[0];\n\t\tpath[i * 2] = lon;\n\t\tpath[i * 2 + 1] = lat;\n\n\t\tif (i > 0) {\n\t\t\tgetSpacialData(path, i, segmentDistances, headings);\n\t\t\ttotalDistance += segmentDistances[i - 1];\n\t\t}\n\t}\n\n\tif (count > 1) headings[count - 1] = headings[count - 2];\n\n\tconst invTotalDist = totalDistance === 0 ? 0 : 1 / totalDistance;\n\tlet cumulativeDist = 0;\n\tfor (let i = 0; i < count; i++) {\n\t\ttimestamps[i] = departure + cumulativeDist * invTotalDist * totalDuration;\n\t\tcumulativeDist += segmentDistances[i];\n\t}\n\n\treturn {\n\t\trealtime: s.realTime,\n\t\tmode: s.mode,\n\t\trouteColor: s.routeColor,\n\t\tpath,\n\t\ttimestamps,\n\t\theadings,\n\t\tcurrentIndx: 0,\n\t\tdepartureDelay: departure - new Date(s.scheduledDeparture).getTime(),\n\t\tarrivalDelay: arrival - new Date(s.scheduledArrival).getTime()\n\t};\n};\n\n//STATE UPDATE\nfunction updateState(data: TransferData, colorMode: string) {\n\tlet posIndex = 0;\n\tlet colorIndex = 0;\n\tlet angleIndex = 0;\n\tconst time = Date.now();\n\tlet color;\n\ttripsMap.forEach((t) => {\n\t\tconst stamps = t.timestamps;\n\t\tconst path = t.path;\n\t\tconst headings = t.headings;\n\t\tconst len = stamps.length;\n\n\t\tswitch (colorMode) {\n\t\t\tcase 'rt':\n\t\t\t\tcolor = getDelayColor(t.departureDelay, t.realtime);\n\t\t\t\tbreak;\n\t\t\tcase 'mode':\n\t\t\t\tcolor = hexToRgb(getModeStyle(t)[1]);\n\t\t\t\tbreak;\n\t\t\tcase 'route':\n\t\t\t\tcolor = hexToRgb(getColor(t)[0]);\n\t\t\t\tbreak;\n\t\t\tcase 'none':\n\t\t\t\tcolor = hexToRgb(getColor(t)[0]);\n\t\t\t\tbreak;\n\t\t}\n\t\tdata.colors[colorIndex] = color![0];\n\t\tdata.colors[colorIndex + 1] = color![1];\n\t\tdata.colors[colorIndex + 2] = color![2];\n\n\t\tlet curr = t.currentIndx;\n\t\twhile (curr < len - 1 && stamps[curr] < time) {\n\t\t\tcurr++;\n\t\t}\n\t\tt.currentIndx = curr;\n\n\t\tconst last = stamps[len - 1];\n\n\t\tif (curr === 0) {\n\t\t\tdata.positions[posIndex] = path[0];\n\t\t\tdata.positions[posIndex + 1] = path[1];\n\t\t\tdata.angles[angleIndex] = headings[0];\n\t\t} else if (curr === len - 1 && time >= last) {\n\t\t\tconst idx = 2 * (len - 1);\n\t\t\tdata.positions[posIndex] = path[idx];\n\t\t\tdata.positions[posIndex + 1] = path[idx + 1];\n\t\t\tdata.angles[angleIndex] = headings[len - 1];\n\t\t} else if (last > time) {\n\t\t\tconst t0 = stamps[curr - 1];\n\t\t\tconst t1 = stamps[curr];\n\n\t\t\tconst r = (time - t0) / (t1 - t0);\n\n\t\t\tconst prevIdx = 2 * (curr - 1);\n\t\t\tconst nextIdx = 2 * curr;\n\n\t\t\tconst x0 = path[prevIdx];\n\t\t\tconst y0 = path[prevIdx + 1];\n\n\t\t\tconst x1 = path[nextIdx];\n\t\t\tconst y1 = path[nextIdx + 1];\n\n\t\t\tdata.positions[posIndex] = x0 + (x1 - x0) * r;\n\t\t\tdata.positions[posIndex + 1] = y0 + (y1 - y0) * r;\n\t\t\tdata.angles[angleIndex] = headings[curr - 1];\n\t\t}\n\n\t\tcolorIndex += 3;\n\t\tangleIndex++;\n\t\tposIndex += 2;\n\t});\n\n\tdata.length = angleIndex;\n}\n\n//MESSAGING\nself.onmessage = async (e) => {\n\tswitch (e.data.type) {\n\t\tcase 'init': {\n\t\t\tconst querySerializer = { array: { explode: false } } as QuerySerializerOptions;\n\t\t\tclient.setConfig({ baseUrl: e.data.baseUrl, querySerializer });\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fetch':\n\t\t\tawait fetchData({ query: e.data.query });\n\t\t\tpostMessage({ type: 'fetch-complete', status });\n\t\t\tbreak;\n\t\tcase 'update': {\n\t\t\tconst positions = new Float64Array(e.data.positions.buffer);\n\t\t\tconst angles = new Float32Array(e.data.angles.buffer);\n\t\t\tconst colors = new Uint8Array(e.data.colors.buffer);\n\t\t\tconst hovIndex = e.data.index;\n\t\t\tconst data = {\n\t\t\t\tcolors,\n\t\t\t\tpositions,\n\t\t\t\tangles,\n\t\t\t\tlength: 0\n\t\t\t};\n\t\t\tupdateState(data, e.data.colorMode);\n\t\t\tpostMessage(\n\t\t\t\t{\n\t\t\t\t\tangles: data.angles,\n\t\t\t\t\tpositions: data.positions,\n\t\t\t\t\tcolors: data.colors,\n\t\t\t\t\tlength: data.length,\n\t\t\t\t\tmetadata: metadata[hovIndex],\n\t\t\t\t\tmetadataIndex: hovIndex\n\t\t\t\t},\n\t\t\t\t[data.angles.buffer, data.positions.buffer, data.colors.buffer]\n\t\t\t);\n\t\t\tbreak;\n\t\t}\n\t}\n};\n\nexport {};\n"
  },
  {
    "path": "ui/src/lib/types.ts",
    "content": "import type { Mode } from '@motis-project/motis-client';\n\nexport type Position = [number, number];\nexport type Trip = {\n\trealtime: boolean;\n\trouteColor?: string;\n\tmode: Mode;\n\tdepartureDelay: number;\n\tpath: Float64Array;\n\tarrivalDelay: number;\n\ttimestamps: Float64Array;\n\tcurrentIndx: number;\n\theadings: Float32Array;\n};\nexport type TransferData = {\n\tlength: number;\n\tpositions: Float64Array;\n\tangles: Float32Array;\n\tcolors: Uint8Array;\n};\nexport type MetaData = {\n\tid: string;\n\tdisplayName?: string;\n\ttz?: string;\n\tfrom: string;\n\tto: string;\n\trealtime: boolean;\n\tarrival: string;\n\tdeparture: string;\n\tscheduledArrival: string;\n\tscheduledDeparture: string;\n\tdepartureDelay: number;\n\tarrivalDelay: number;\n};\n"
  },
  {
    "path": "ui/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\nimport { browser } from '$app/environment';\nimport { pushState, replaceState } from '$app/navigation';\nimport { page } from '$app/state';\nimport { trip } from '@motis-project/motis-client';\nimport { joinInterlinedLegs } from './preprocessItinerary';\nimport { language } from './i18n/translation';\nimport { tick } from 'svelte';\n\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst urlParams = browser ? new URLSearchParams(window.location.search) : undefined;\n\nexport const getUrlArray = (key: string, defaultValue?: string[]): string[] => {\n\tif (urlParams) {\n\t\tconst value = urlParams.get(key);\n\t\tif (value != null) {\n\t\t\treturn value.split(',').filter((m) => m.length);\n\t\t}\n\t}\n\tif (defaultValue) {\n\t\treturn defaultValue;\n\t}\n\treturn [];\n};\n\nexport const preserveFromUrl = (\n\t// eslint-disable-next-line\n\tqueryParams: Record<string, any>,\n\tfield: string\n) => {\n\tif (urlParams?.has(field)) {\n\t\tqueryParams[field] = urlParams.get(field);\n\t}\n};\n\nexport const pushStateWithQueryString = async (\n\t// eslint-disable-next-line\n\tqueryParams: Record<string, any>,\n\n\tnewState: App.PageState,\n\treplace: boolean = false\n) => {\n\tpreserveFromUrl(queryParams, 'debug');\n\tpreserveFromUrl(queryParams, 'dark');\n\tpreserveFromUrl(queryParams, 'light');\n\tpreserveFromUrl(queryParams, 'motis');\n\tpreserveFromUrl(queryParams, 'language');\n\tconst params = new URLSearchParams(queryParams);\n\tconst updateState = replace ? replaceState : pushState;\n\ttry {\n\t\tupdateState('?' + params.toString(), newState);\n\t} catch (e) {\n\t\tconsole.log(e);\n\t\tawait tick();\n\t\tupdateState('?' + params.toString(), newState);\n\t}\n};\n\nexport const closeItinerary = () => {\n\tif (page.state.selectedStop) {\n\t\tonClickStop(\n\t\t\tpage.state.selectedStop.name,\n\t\t\tpage.state.selectedStop.stopId,\n\t\t\tpage.state.selectedStop.time,\n\t\t\tpage.state.stopArriveBy ?? false,\n\t\t\ttrue\n\t\t);\n\t\treturn;\n\t}\n\n\tpushStateWithQueryString({}, {});\n};\n\nexport const onClickStop = (\n\tname: string,\n\tstopId: string,\n\ttime: Date,\n\tarriveBy: boolean = false,\n\treplace: boolean = false\n) => {\n\tpushStateWithQueryString(\n\t\t{ stopArriveBy: arriveBy, stopId, time: time.toISOString() },\n\t\t{\n\t\t\tstopArriveBy: arriveBy,\n\t\t\tselectedStop: { name, stopId, time },\n\t\t\tselectedItinerary: replace ? undefined : page.state.selectedItinerary,\n\t\t\ttripId: replace ? undefined : page.state.tripId,\n\t\t\tactiveTab: 'departures'\n\t\t},\n\t\treplace\n\t);\n};\n\nexport const onClickTrip = async (tripId: string, replace: boolean = false) => {\n\tconst { data: itinerary, error } = await trip({\n\t\tquery: { tripId, joinInterlinedLegs: false, language: [language] }\n\t});\n\tif (error) {\n\t\tconsole.log(error);\n\t\talert(String((error as Record<string, unknown>).error?.toString() ?? error));\n\t\treturn;\n\t}\n\tjoinInterlinedLegs(itinerary!);\n\tpushStateWithQueryString(\n\t\t{ tripId },\n\t\t{\n\t\t\tselectedItinerary: itinerary,\n\t\t\ttripId: tripId,\n\t\t\tselectedStop: replace ? undefined : page.state.selectedStop,\n\t\t\tactiveTab: 'connections'\n\t\t},\n\t\treplace\n\t);\n};\n"
  },
  {
    "path": "ui/src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n\timport '../app.css';\n\timport { browser } from '$app/environment';\n\timport { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';\n\n\tconst { children } = $props();\n\n\tconst queryClient = new QueryClient({\n\t\tdefaultOptions: {\n\t\t\tqueries: {\n\t\t\t\tenabled: browser\n\t\t\t}\n\t\t}\n\t});\n</script>\n\n<QueryClientProvider client={queryClient}>\n\t{@render children()}\n</QueryClientProvider>\n"
  },
  {
    "path": "ui/src/routes/+layout.ts",
    "content": "import { client } from '@motis-project/motis-client';\nimport { browser } from '$app/environment';\nimport type { QuerySerializerOptions } from '@hey-api/client-fetch';\n\nexport const prerender = true;\n\nif (browser) {\n\tconst params = new URL(window.location.href).searchParams;\n\tconst defaultProtocol = window.location.protocol;\n\tconst defaultHost = window.location.hostname;\n\tconst defaultPort = '8080';\n\tconst motisParam = params.get('motis');\n\tlet baseUrl = String(window.location.origin + window.location.pathname);\n\tif (motisParam) {\n\t\tif (/^[0-9]+$/.test(motisParam)) {\n\t\t\tbaseUrl = defaultProtocol + '//' + defaultHost + ':' + motisParam;\n\t\t} else if (!motisParam.includes(':')) {\n\t\t\tbaseUrl = defaultProtocol + '//' + motisParam + ':' + defaultPort;\n\t\t} else if (!motisParam.startsWith('http:') && !motisParam.startsWith('https:')) {\n\t\t\tbaseUrl = defaultProtocol + '//' + motisParam;\n\t\t} else {\n\t\t\tbaseUrl = motisParam;\n\t\t}\n\t}\n\tconst querySerializer = { array: { explode: false } } as QuerySerializerOptions;\n\tclient.setConfig({ baseUrl, querySerializer }); //`${window.location}`\n}\n"
  },
  {
    "path": "ui/src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport {\n\t\tX,\n\t\tPalette,\n\t\tRss,\n\t\tBan,\n\t\tLocateFixed,\n\t\tTrainFront,\n\t\tWaypoints,\n\t\tMountainSnow\n\t} from '@lucide/svelte';\n\timport { getStyle } from '$lib/map/style';\n\timport Map from '$lib/map/Map.svelte';\n\timport Control from '$lib/map/Control.svelte';\n\timport SearchMask from '$lib/SearchMask.svelte';\n\timport { parseLocation, posToLocation, type Location } from '$lib/Location';\n\timport { Card } from '$lib/components/ui/card';\n\timport {\n\t\tinitial,\n\t\toneToAll,\n\t\tplan,\n\t\ttype ElevationCosts,\n\t\ttype OneToAllData,\n\t\ttype PlanResponse,\n\t\ttype Itinerary,\n\t\ttype Mode,\n\t\ttype PedestrianProfile,\n\t\ttype PlanData,\n\t\ttype ReachablePlace,\n\t\ttype RentalFormFactor,\n\t\ttype ServerConfig\n\t} from '@motis-project/motis-client';\n\timport ItineraryList from '$lib/ItineraryList.svelte';\n\timport ConnectionDetail from '$lib/ConnectionDetail.svelte';\n\timport { Button } from '$lib/components/ui/button';\n\timport ItineraryGeoJson from '$lib/map/itineraries/ItineraryGeoJSON.svelte';\n\timport maplibregl from 'maplibre-gl';\n\timport { browser } from '$app/environment';\n\timport { cn, getUrlArray, onClickStop, onClickTrip, pushStateWithQueryString } from '$lib/utils';\n\timport Debug from '$lib/Debug.svelte';\n\timport Marker from '$lib/map/Marker.svelte';\n\timport Popup from '$lib/map/Popup.svelte';\n\timport LevelSelect from '$lib/LevelSelect.svelte';\n\timport { lngLatToStr } from '$lib/lngLatToStr';\n\timport Drawer from '$lib/map/Drawer.svelte';\n\timport { client } from '@motis-project/motis-client';\n\timport StopTimes from '$lib/StopTimes.svelte';\n\timport { onMount, tick, untrack } from 'svelte';\n\timport { t } from '$lib/i18n/translation';\n\timport { pushState } from '$app/navigation';\n\timport { page } from '$app/state';\n\timport { preprocessItinerary } from '$lib/preprocessItinerary';\n\timport * as Tabs from '$lib/components/ui/tabs';\n\timport DeparturesMask from '$lib/DeparturesMask.svelte';\n\timport Isochrones from '$lib/map/Isochrones.svelte';\n\timport IsochronesInfo from '$lib/IsochronesInfo.svelte';\n\timport type { DisplayLevel, IsochronesOptions, IsochronesPos } from '$lib/map/IsochronesShared';\n\timport IsochronesMask from '$lib/IsochronesMask.svelte';\n\timport Rentals from '$lib/map/rentals/Rentals.svelte';\n\timport Routes from '$lib/map/routes/Routes.svelte';\n\timport {\n\t\tgetFormFactors,\n\t\tgetPrePostDirectModes,\n\t\tpossibleTransitModes,\n\t\tprePostModesToModes,\n\t\ttype PrePostDirectMode\n\t} from '$lib/Modes';\n\timport { defaultQuery, omitDefaults } from '$lib/defaults';\n\timport { LEVEL_MIN_ZOOM } from '$lib/constants';\n\timport StopGeoJSON from '$lib/map/stops/StopsGeoJSON.svelte';\n\timport RailViz from '$lib/RailViz.svelte';\n\n\tconst urlParams = browser ? new URLSearchParams(window.location.search) : undefined;\n\n\tconst hasDebug: boolean = Boolean(urlParams?.has('debug'));\n\tconst hasDark: boolean = Boolean(urlParams?.has('dark'));\n\tconst hasLight: boolean = Boolean(urlParams?.has('light'));\n\tconst isSmallScreen = browser && window.innerWidth < 768;\n\tlet activeTab = $derived<'connections' | 'departures' | 'isochrones'>(\n\t\tpage.state.activeTab ??\n\t\t\t(urlParams?.has('one')\n\t\t\t\t? 'isochrones'\n\t\t\t\t: urlParams?.has('stopId')\n\t\t\t\t\t? 'departures'\n\t\t\t\t\t: 'connections')\n\t);\n\tlet dataAttributionLink: string | undefined = $state(undefined);\n\tlet colorMode = $state<'rt' | 'route' | 'mode' | 'none'>(isSmallScreen ? 'none' : 'rt');\n\tlet showMap = $state(!isSmallScreen);\n\tlet showRoutes = $state(false);\n\tlet routesOverlaySession = $state(0);\n\tlet lastOneToAllQuery: OneToAllData | undefined = undefined;\n\tlet lastPlanQuery: PlanData | undefined = undefined;\n\tlet serverConfig: ServerConfig | undefined = $state();\n\tlet dataLoaded: boolean = $state(false);\n\n\t$effect(() => {\n\t\tif (activeTab == 'isochrones') {\n\t\t\tcolorMode = 'none';\n\t\t}\n\t});\n\n\tconst toggleRoutesOverlay = () => {\n\t\t++routesOverlaySession;\n\t\tshowRoutes = !showRoutes;\n\t};\n\n\tlet theme: 'light' | 'dark' =\n\t\t(hasDark ? 'dark' : hasLight ? 'light' : undefined) ??\n\t\t(browser && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches\n\t\t\t? 'dark'\n\t\t\t: 'light');\n\tif (theme === 'dark') {\n\t\tdocument.documentElement.classList.add('dark');\n\t}\n\n\tlet withHillshades = $state(false);\n\tlet center = $state.raw<[number, number]>([2.258882912876089, 48.72559118651327]);\n\tlet level = $state(0);\n\tlet zoom = $state(15);\n\tlet bounds = $state<maplibregl.LngLatBoundsLike>();\n\tlet map = $state<maplibregl.Map>();\n\tlet style = $derived(\n\t\tbrowser\n\t\t\t? getStyle(\n\t\t\t\t\ttheme,\n\t\t\t\t\tlevel,\n\t\t\t\t\twindow.location.origin + window.location.pathname,\n\t\t\t\t\tclient.getConfig().baseUrl\n\t\t\t\t\t\t? client.getConfig().baseUrl + '/'\n\t\t\t\t\t\t: window.location.origin + window.location.pathname,\n\t\t\t\t\twithHillshades\n\t\t\t\t)\n\t\t\t: undefined\n\t);\n\n\tconst geolocate = new maplibregl.GeolocateControl({\n\t\tpositionOptions: {\n\t\t\tenableHighAccuracy: true\n\t\t},\n\t\tshowAccuracyCircle: false\n\t});\n\n\tconst getLocation = () => {\n\t\tgeolocate.trigger();\n\t};\n\n\tonMount(async () => {\n\t\tinitial().then((d) => {\n\t\t\tif (d.response.headers.has('Link')) {\n\t\t\t\tdataAttributionLink = d.response.headers\n\t\t\t\t\t.get('Link')!\n\t\t\t\t\t.replace(/^<(.*)>; rel=\"license\"$/, '$1');\n\t\t\t}\n\t\t\tconst r = d.data;\n\t\t\tif (r) {\n\t\t\t\tcenter = [r.lon, r.lat];\n\t\t\t\tzoom = r.zoom;\n\t\t\t\tserverConfig = r.serverConfig;\n\t\t\t}\n\t\t\tdataLoaded = true;\n\t\t});\n\t\tawait tick();\n\t\tapplyPageStateFromURL();\n\t});\n\n\tconst applyPageStateFromURL = () => {\n\t\tif (browser && urlParams) {\n\t\t\tconst tripId = urlParams.get('tripId');\n\t\t\tif (tripId !== null) {\n\t\t\t\tonClickTrip(tripId, true);\n\t\t\t}\n\n\t\t\tconst stopId = urlParams.get('stopId');\n\t\t\tif (stopId !== null) {\n\t\t\t\tconst time = urlParams.has('time') ? new Date(urlParams.get('time')!) : new Date();\n\t\t\t\tonClickStop('', stopId, time, urlParams.get('stopArriveBy') == 'true', true);\n\t\t\t}\n\t\t}\n\t};\n\n\tfunction parseIntOr<T>(s: string | null | undefined, d: T): T | number {\n\t\tif (s) {\n\t\t\tconst v = parseInt(s);\n\t\t\treturn isNaN(v) ? d : v;\n\t\t} else {\n\t\t\treturn d;\n\t\t}\n\t}\n\n\tlet fromMarker = $state<maplibregl.Marker>();\n\tlet toMarker = $state<maplibregl.Marker>();\n\tlet oneMarker = $state<maplibregl.Marker>();\n\tlet stopMarker = $state<maplibregl.Marker>();\n\tlet from = $state<Location>(\n\t\tparseLocation(urlParams?.get('fromPlace'), urlParams?.get('fromName'))\n\t);\n\tlet to = $state<Location>(parseLocation(urlParams?.get('toPlace'), urlParams?.get('toName')));\n\tlet one = $state<Location>(parseLocation(urlParams?.get('one'), urlParams?.get('oneName')));\n\tlet stop = $state<Location>();\n\n\tlet viaParam = getUrlArray('via');\n\tlet viaLabels = $state(\n\t\turlParams?.has('viaLabel0')\n\t\t\t? Array.from({ length: viaParam.length }).reduce<Record<string, string>>((acc, _, i) => {\n\t\t\t\t\tacc[`viaLabel${i}`] = urlParams?.get(`viaLabel${i}`) ?? '';\n\t\t\t\t\treturn acc;\n\t\t\t\t}, {})\n\t\t\t: {}\n\t);\n\tlet via = $state(\n\t\turlParams?.has('via')\n\t\t\t? viaParam.map((str, i) => parseLocation(str, viaLabels[`viaLabel${i}`]))\n\t\t\t: undefined\n\t);\n\tlet viaMinimumStay = $state(\n\t\turlParams?.has('via') ? getUrlArray('viaMinimumStay').map((s) => parseIntOr(s, 0)) : undefined\n\t);\n\n\tlet time = $state<Date>(new Date(urlParams?.get('time') || Date.now()));\n\tlet timetableView = $state(urlParams?.get('timetableView') != 'false');\n\tlet searchWindow = $state(\n\t\turlParams?.get('searchWindow')\n\t\t\t? parseInt(urlParams.get('searchWindow')!)\n\t\t\t: defaultQuery.searchWindow\n\t);\n\tlet numItineraries = $state(\n\t\turlParams?.get('numItineraries')\n\t\t\t? parseIntOr(urlParams.get('numItineraries'), defaultQuery.numItineraries)\n\t\t\t: defaultQuery.numItineraries\n\t);\n\tlet maxItineraries = $state(\n\t\turlParams?.get('maxItineraries')\n\t\t\t? parseIntOr(urlParams.get('maxItineraries'), undefined)\n\t\t\t: undefined\n\t);\n\tlet arriveBy = $state<boolean>(urlParams?.get('arriveBy') == 'true');\n\tlet algorithm = $state<PlanData['query']['algorithm']>(\n\t\t(urlParams?.get('algorithm') ?? defaultQuery.algorithm) as PlanData['query']['algorithm']\n\t);\n\tlet useRoutedTransfers = $state(\n\t\turlParams?.get('useRoutedTransfers') == 'true' || defaultQuery.useRoutedTransfers\n\t);\n\tlet pedestrianProfile = $state<PedestrianProfile>(\n\t\t(urlParams?.has('pedestrianProfile')\n\t\t\t? urlParams.get('pedestrianProfile')\n\t\t\t: defaultQuery.pedestrianProfile) as PedestrianProfile\n\t);\n\tlet requireBikeTransport = $state(urlParams?.get('requireBikeTransport') == 'true');\n\tlet requireCarTransport = $state(urlParams?.get('requireCarTransport') == 'true');\n\tlet transitModes = $state<Mode[]>(\n\t\tgetUrlArray('transitModes', defaultQuery.transitModes) as Mode[]\n\t);\n\tlet preTransitModes = $state<PrePostDirectMode[]>(\n\t\tgetPrePostDirectModes(\n\t\t\tgetUrlArray('preTransitModes', defaultQuery.preTransitModes) as Mode[],\n\t\t\tgetUrlArray('preTransitRentalFormFactors') as RentalFormFactor[]\n\t\t)\n\t);\n\tlet postTransitModes = $state<PrePostDirectMode[]>(\n\t\tgetPrePostDirectModes(\n\t\t\tgetUrlArray('postTransitModes', defaultQuery.postTransitModes) as Mode[],\n\t\t\tgetUrlArray('postTransitRentalFormFactors') as RentalFormFactor[]\n\t\t)\n\t);\n\tlet directModes = $state<PrePostDirectMode[]>(\n\t\tgetPrePostDirectModes(\n\t\t\tgetUrlArray('directModes', defaultQuery.directModes) as Mode[],\n\t\t\tgetUrlArray('directRentalFormFactors') as RentalFormFactor[]\n\t\t)\n\t);\n\tlet preTransitProviderGroups = $state<string[]>(getUrlArray('preTransitRentalProviderGroups'));\n\tlet postTransitProviderGroups = $state<string[]>(getUrlArray('postTransitRentalProviderGroups'));\n\tlet directProviderGroups = $state<string[]>(getUrlArray('directRentalProviderGroups'));\n\tlet elevationCosts = $state<ElevationCosts>(\n\t\t(urlParams?.get('elevationCosts') ?? 'NONE') as ElevationCosts\n\t);\n\tlet maxTransfers = $state<number>(\n\t\tparseIntOr(urlParams?.get('maxTransfers'), defaultQuery.maxTransfers)\n\t);\n\tlet maxTravelTime = $derived<number>(\n\t\tparseIntOr(\n\t\t\turlParams?.get('maxTravelTime'),\n\t\t\tMath.min(\n\t\t\t\tdefaultQuery.maxTravelTime,\n\t\t\t\t60 * (serverConfig?.maxOneToAllTravelTimeLimit ?? Infinity)\n\t\t\t)\n\t\t)\n\t);\n\tlet maxPreTransitTime = $derived<number>(\n\t\tparseIntOr(\n\t\t\turlParams?.get('maxPreTransitTime'),\n\t\t\tMath.min(defaultQuery.maxPreTransitTime, serverConfig?.maxPrePostTransitTimeLimit ?? Infinity)\n\t\t)\n\t);\n\tlet maxPostTransitTime = $derived<number>(\n\t\tparseIntOr(\n\t\t\turlParams?.get('maxPostTransitTime'),\n\t\t\tMath.min(\n\t\t\t\tdefaultQuery.maxPostTransitTime,\n\t\t\t\tserverConfig?.maxPrePostTransitTimeLimit ?? Infinity\n\t\t\t)\n\t\t)\n\t);\n\tlet maxDirectTime = $derived<number>(\n\t\tparseIntOr(\n\t\t\turlParams?.get('maxDirectTime'),\n\t\t\tMath.min(defaultQuery.maxDirectTime, serverConfig?.maxDirectTimeLimit ?? Infinity)\n\t\t)\n\t);\n\tlet ignorePreTransitRentalReturnConstraints = $state(\n\t\turlParams?.get('ignorePreTransitRentalReturnConstraints') == 'true'\n\t);\n\tlet ignorePostTransitRentalReturnConstraints = $state(\n\t\turlParams?.get('ignorePostTransitRentalReturnConstraints') == 'true'\n\t);\n\tlet ignoreDirectRentalReturnConstraints = $state(\n\t\turlParams?.get('ignoreDirectRentalReturnConstraints') == 'true'\n\t);\n\tlet slowDirect = $state(urlParams?.get('slowDirect') == 'true');\n\n\tlet isochronesData = $state<IsochronesPos[]>([]);\n\tlet isochronesOptions = $state<IsochronesOptions>({\n\t\tdisplayLevel:\n\t\t\t(urlParams?.get('isochronesDisplayLevel') as DisplayLevel) ??\n\t\t\tdefaultQuery.isochronesDisplayLevel,\n\t\tcolor: urlParams?.get('isochronesColor') ?? defaultQuery.isochronesColor,\n\t\topacity: parseIntOr(urlParams?.get('isochronesOpacity'), defaultQuery.isochronesOpacity),\n\t\tstatus: 'DONE',\n\t\terrorMessage: undefined,\n\t\terrorCode: undefined\n\t});\n\tconst isochronesCircleResolution = urlParams?.get('isochronesCircleResolution')\n\t\t? parseIntOr(urlParams.get('isochronesCircleResolution'), defaultQuery.circleResolution)\n\t\t: defaultQuery.circleResolution;\n\n\tconst toPlaceString = (l: Location) => {\n\t\tif (l.match?.type === 'STOP') {\n\t\t\treturn l.match.id;\n\t\t} else if (l.match?.level) {\n\t\t\treturn `${lngLatToStr(l.match!)},${l.match.level}`;\n\t\t} else {\n\t\t\treturn `${lngLatToStr(l.match!)}`;\n\t\t}\n\t};\n\n\tconst providerGroupsForQuery = (modes: PrePostDirectMode[], groups: string[]): string[] => {\n\t\tif (!modes.some((mode) => mode.startsWith('RENTAL_'))) {\n\t\t\treturn [];\n\t\t}\n\t\treturn Array.from(new Set(groups));\n\t};\n\n\tlet baseQuery = $derived(\n\t\tfrom.match && to.match\n\t\t\t? ({\n\t\t\t\t\tquery: omitDefaults({\n\t\t\t\t\t\ttime: time.toISOString(),\n\t\t\t\t\t\tfromPlace: toPlaceString(from),\n\t\t\t\t\t\ttoPlace: toPlaceString(to),\n\t\t\t\t\t\tarriveBy,\n\t\t\t\t\t\ttimetableView,\n\t\t\t\t\t\tsearchWindow,\n\t\t\t\t\t\tnumItineraries,\n\t\t\t\t\t\tmaxItineraries,\n\t\t\t\t\t\twithFares: true,\n\t\t\t\t\t\tslowDirect,\n\t\t\t\t\t\tfastestDirectFactor: 1.5,\n\t\t\t\t\t\tpedestrianProfile,\n\t\t\t\t\t\tjoinInterlinedLegs: false,\n\t\t\t\t\t\ttransitModes:\n\t\t\t\t\t\t\ttransitModes.length == possibleTransitModes.length\n\t\t\t\t\t\t\t\t? defaultQuery.transitModes\n\t\t\t\t\t\t\t\t: transitModes,\n\t\t\t\t\t\tpreTransitModes: prePostModesToModes(preTransitModes),\n\t\t\t\t\t\tpostTransitModes: prePostModesToModes(postTransitModes),\n\t\t\t\t\t\tdirectModes: prePostModesToModes(directModes),\n\t\t\t\t\t\tpreTransitRentalFormFactors: getFormFactors(preTransitModes),\n\t\t\t\t\t\tpostTransitRentalFormFactors: getFormFactors(postTransitModes),\n\t\t\t\t\t\tdirectRentalFormFactors: getFormFactors(directModes),\n\t\t\t\t\t\tpreTransitRentalProviderGroups: providerGroupsForQuery(\n\t\t\t\t\t\t\tpreTransitModes,\n\t\t\t\t\t\t\tpreTransitProviderGroups\n\t\t\t\t\t\t),\n\t\t\t\t\t\tpostTransitRentalProviderGroups: providerGroupsForQuery(\n\t\t\t\t\t\t\tpostTransitModes,\n\t\t\t\t\t\t\tpostTransitProviderGroups\n\t\t\t\t\t\t),\n\t\t\t\t\t\tdirectRentalProviderGroups: providerGroupsForQuery(directModes, directProviderGroups),\n\t\t\t\t\t\trequireBikeTransport,\n\t\t\t\t\t\trequireCarTransport,\n\t\t\t\t\t\televationCosts,\n\t\t\t\t\t\tuseRoutedTransfers,\n\t\t\t\t\t\tmaxTransfers: maxTransfers,\n\t\t\t\t\t\tmaxMatchingDistance: pedestrianProfile == 'WHEELCHAIR' ? 8 : 250,\n\t\t\t\t\t\tmaxPreTransitTime,\n\t\t\t\t\t\tmaxPostTransitTime,\n\t\t\t\t\t\tmaxDirectTime,\n\t\t\t\t\t\tignorePreTransitRentalReturnConstraints,\n\t\t\t\t\t\tignorePostTransitRentalReturnConstraints,\n\t\t\t\t\t\tignoreDirectRentalReturnConstraints,\n\t\t\t\t\t\talgorithm,\n\t\t\t\t\t\tvia: via ? via.map((v) => v.match?.id) : undefined,\n\t\t\t\t\t\tviaMinimumStay\n\t\t\t\t\t} as PlanData['query'])\n\t\t\t\t} as PlanData)\n\t\t\t: undefined\n\t);\n\n\tlet isochronesQuery = $derived(\n\t\tone?.match\n\t\t\t? ({\n\t\t\t\t\tquery: {\n\t\t\t\t\t\tone: toPlaceString(one),\n\t\t\t\t\t\tmaxTravelTime: Math.ceil(maxTravelTime / 60),\n\t\t\t\t\t\ttime: time.toISOString(),\n\t\t\t\t\t\ttransitModes,\n\t\t\t\t\t\tmaxTransfers,\n\t\t\t\t\t\tarriveBy,\n\t\t\t\t\t\tuseRoutedTransfers,\n\t\t\t\t\t\tpedestrianProfile,\n\t\t\t\t\t\trequireBikeTransport,\n\t\t\t\t\t\trequireCarTransport,\n\t\t\t\t\t\tpreTransitModes: prePostModesToModes(preTransitModes),\n\t\t\t\t\t\tpostTransitModes: prePostModesToModes(postTransitModes),\n\t\t\t\t\t\tmaxPreTransitTime,\n\t\t\t\t\t\tmaxPostTransitTime,\n\t\t\t\t\t\televationCosts,\n\t\t\t\t\t\tmaxMatchingDistance: pedestrianProfile == 'WHEELCHAIR' ? 8 : 250,\n\t\t\t\t\t\tignorePreTransitRentalReturnConstraints,\n\t\t\t\t\t\tignorePostTransitRentalReturnConstraints\n\t\t\t\t\t}\n\t\t\t\t} as OneToAllData)\n\t\t\t: undefined\n\t);\n\n\tlet searchDebounceTimer: number;\n\tlet baseResponse = $state<Promise<PlanResponse>>();\n\tlet routingResponses = $state<Array<Promise<PlanResponse>>>([]);\n\tlet stopNameFromResponse = $state<string>('');\n\t$effect(() => {\n\t\tif (baseQuery && baseQuery != lastPlanQuery && activeTab == 'connections') {\n\t\t\tlastPlanQuery = baseQuery;\n\t\t\tclearTimeout(searchDebounceTimer);\n\t\t\tsearchDebounceTimer = setTimeout(() => {\n\t\t\t\tconst base = plan(baseQuery).then(preprocessItinerary(from, to));\n\t\t\t\tconst q = baseQuery.query;\n\t\t\t\tbaseResponse = base;\n\t\t\t\troutingResponses = [base];\n\t\t\t\tpushStateWithQueryString(\n\t\t\t\t\t{\n\t\t\t\t\t\t...q,\n\t\t\t\t\t\t...(q.fromPlace == from.label ? {} : { fromName: from.label }),\n\t\t\t\t\t\t...(q.toPlace == to.label ? {} : { toName: to.label }),\n\t\t\t\t\t\t...viaLabels\n\t\t\t\t\t},\n\t\t\t\t\t{ activeTab: 'connections' },\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t}, 400);\n\t\t}\n\t});\n\tlet isochronesQueryTimeout: number;\n\t$effect(() => {\n\t\tif (isochronesQuery && activeTab == 'isochrones') {\n\t\t\tconst [isochronesColor, isochronesOpacity, isochronesDisplayLevel] = [\n\t\t\t\tisochronesOptions.color,\n\t\t\t\tisochronesOptions.opacity,\n\t\t\t\tisochronesOptions.displayLevel\n\t\t\t];\n\t\t\tif (lastOneToAllQuery != isochronesQuery) {\n\t\t\t\tlastOneToAllQuery = isochronesQuery;\n\t\t\t\tclearTimeout(isochronesQueryTimeout);\n\t\t\t\tisochronesOptions.status = 'WORKING';\n\t\t\t\tisochronesOptions.errorMessage = undefined;\n\t\t\t\tisochronesQueryTimeout = setTimeout(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst { data, error, response } = await oneToAll(isochronesQuery);\n\t\t\t\t\t\tif (error) {\n\t\t\t\t\t\t\tisochronesOptions.status = 'FAILED';\n\t\t\t\t\t\t\tisochronesOptions.errorCode = response.status;\n\t\t\t\t\t\t\tisochronesOptions.errorMessage = error.error;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst all = data!.all!.map((p: ReachablePlace) => {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tlat: p.place?.lat,\n\t\t\t\t\t\t\t\tlng: p.place?.lon,\n\t\t\t\t\t\t\t\tseconds: maxTravelTime - 60 * (p.duration ?? 0),\n\t\t\t\t\t\t\t\tname: p.place?.name\n\t\t\t\t\t\t\t} as IsochronesPos;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tisochronesData = [...all];\n\t\t\t\t\t\tisochronesOptions.status = isochronesData.length == 0 ? 'EMPTY' : 'WORKING';\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tisochronesOptions.status = 'FAILED';\n\t\t\t\t\t\tisochronesOptions.errorMessage = String(e);\n\t\t\t\t\t\tisochronesOptions.errorCode = 404;\n\t\t\t\t\t}\n\t\t\t\t}, 60);\n\t\t\t}\n\t\t\tuntrack(() => {\n\t\t\t\tconst q = isochronesQuery.query;\n\t\t\t\tpushStateWithQueryString(\n\t\t\t\t\t{\n\t\t\t\t\t\t...q,\n\t\t\t\t\t\t...(q.one == one.label ? {} : { oneName: one.label }),\n\t\t\t\t\t\tmaxTravelTime: q.maxTravelTime * 60,\n\t\t\t\t\t\tisochronesColor,\n\t\t\t\t\t\tisochronesOpacity,\n\t\t\t\t\t\tisochronesDisplayLevel,\n\t\t\t\t\t\t...(isochronesCircleResolution && isochronesCircleResolution > 2\n\t\t\t\t\t\t\t? { isochronesCircleResolution }\n\t\t\t\t\t\t\t: {})\n\t\t\t\t\t},\n\t\t\t\t\t{ activeTab: 'isochrones' },\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t});\n\t\t}\n\t});\n\n\tif (browser) {\n\t\taddEventListener('paste', (event: ClipboardEvent) => {\n\t\t\tconst paste = event.clipboardData!.getData('text');\n\t\t\tconst json = JSON.parse(paste);\n\t\t\tconsole.log('paste: ', json);\n\t\t\tconst response = new Promise<PlanResponse>((resolve, _) => {\n\t\t\t\tresolve(json as PlanResponse);\n\t\t\t});\n\t\t\tbaseResponse = response;\n\t\t\troutingResponses = [response];\n\t\t});\n\t}\n\n\tconst flyToItineraries = (itineraries: Itinerary[], map: maplibregl.Map) => {\n\t\tconst start = maplibregl.LngLat.convert(itineraries[0].legs[0].from);\n\t\tconst box = new maplibregl.LngLatBounds(start, start);\n\t\titineraries.forEach((i) => {\n\t\t\ti.legs.forEach((l) => {\n\t\t\t\tbox.extend(l.from);\n\t\t\t\tbox.extend(l.to);\n\t\t\t\tl.intermediateStops?.forEach((x) => {\n\t\t\t\t\tbox.extend(x);\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t\tmap.flyTo({\n\t\t\t...map.cameraForBounds(box, {\n\t\t\t\tpadding: {\n\t\t\t\t\ttop: 96,\n\t\t\t\t\tright: 96,\n\t\t\t\t\tbottom: isSmallScreen ? window.innerHeight * 0.3 : 96,\n\t\t\t\t\tleft: isSmallScreen ? 96 : 640\n\t\t\t\t}\n\t\t\t})\n\t\t});\n\t};\n\n\tconst flyToLocation = (location: Location) => {\n\t\tmap?.flyTo({ center: location.match, zoom: 11 });\n\t};\n\n\tconst flyToSelectedItinerary = () => {\n\t\tif (page.state.selectedItinerary && map) {\n\t\t\tflyToItineraries([page.state.selectedItinerary], map);\n\t\t}\n\t};\n\n\t$effect(() => {\n\t\tif (map) {\n\t\t\tmap.addControl(geolocate);\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (map) {\n\t\t\tif (page.state.selectedItinerary && activeTab == 'connections') {\n\t\t\t\tflyToSelectedItinerary();\n\t\t\t} else if (activeTab == 'departures' && stop && stop.match) {\n\t\t\t\tflyToLocation(stop);\n\t\t\t} else if (activeTab == 'isochrones' && one && one.match) {\n\t\t\t\tflyToLocation(one);\n\t\t\t}\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (!map || activeTab != 'connections' || !baseQuery) return;\n\t\tPromise.all(routingResponses).then((responses) => {\n\t\t\tif (map) {\n\t\t\t\tlet it = responses.flatMap((response) => response.itineraries);\n\t\t\t\tif (it.length !== 0) {\n\t\t\t\t\tflyToItineraries(it, map);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\ttype CloseFn = () => void;\n</script>\n\n{#snippet contextMenu(e: maplibregl.MapMouseEvent, close: CloseFn)}\n\t{#if activeTab == 'connections'}\n\t\t<Button\n\t\t\tvariant=\"outline\"\n\t\t\tonclick={() => {\n\t\t\t\tfrom = posToLocation(e.lngLat, zoom > LEVEL_MIN_ZOOM ? level : undefined);\n\t\t\t\tfromMarker?.setLngLat(from.match!);\n\t\t\t\tclose();\n\t\t\t}}\n\t\t>\n\t\t\tFrom\n\t\t</Button>\n\t\t<Button\n\t\t\tvariant=\"outline\"\n\t\t\tonclick={() => {\n\t\t\t\tto = posToLocation(e.lngLat, zoom > LEVEL_MIN_ZOOM ? level : undefined);\n\t\t\t\ttoMarker?.setLngLat(to.match!);\n\t\t\t\tclose();\n\t\t\t}}\n\t\t>\n\t\t\tTo\n\t\t</Button>\n\t{:else if activeTab == 'isochrones'}\n\t\t<Button\n\t\t\tvariant=\"outline\"\n\t\t\tonclick={() => {\n\t\t\t\tone = posToLocation(e.lngLat, zoom > LEVEL_MIN_ZOOM ? level : undefined);\n\t\t\t\toneMarker?.setLngLat(one.match!);\n\t\t\t\tclose();\n\t\t\t}}\n\t\t>\n\t\t\t{t.position}\n\t\t</Button>\n\t{/if}\n{/snippet}\n\n{#snippet resultContent()}\n\t<Control>\n\t\t<Tabs.Root\n\t\t\tbind:value={\n\t\t\t\t() => activeTab,\n\t\t\t\t(v) => {\n\t\t\t\t\tactiveTab = v;\n\t\t\t\t\tpushState('', { activeTab: v });\n\t\t\t\t}\n\t\t\t}\n\t\t\tclass=\"max-w-full w-[520px] overflow-y-auto\"\n\t\t>\n\t\t\t<Tabs.List class=\"grid grid-cols-3\">\n\t\t\t\t<Tabs.Trigger value=\"connections\">{t.connections}</Tabs.Trigger>\n\t\t\t\t<Tabs.Trigger value=\"departures\">{t.departures}</Tabs.Trigger>\n\t\t\t\t<Tabs.Trigger value=\"isochrones\">{t.isochrones.title}</Tabs.Trigger>\n\t\t\t</Tabs.List>\n\t\t\t<Tabs.Content value=\"connections\">\n\t\t\t\t<Card class=\"overflow-y-auto overflow-x-hidden bg-background rounded-lg\">\n\t\t\t\t\t<SearchMask\n\t\t\t\t\t\tgeocodingBiasPlace={center}\n\t\t\t\t\t\t{serverConfig}\n\t\t\t\t\t\tbind:from\n\t\t\t\t\t\tbind:to\n\t\t\t\t\t\tbind:time\n\t\t\t\t\t\tbind:arriveBy\n\t\t\t\t\t\tbind:useRoutedTransfers\n\t\t\t\t\t\tbind:maxTransfers\n\t\t\t\t\t\tbind:pedestrianProfile\n\t\t\t\t\t\tbind:requireCarTransport\n\t\t\t\t\t\tbind:requireBikeTransport\n\t\t\t\t\t\tbind:transitModes\n\t\t\t\t\t\tbind:preTransitModes\n\t\t\t\t\t\tbind:postTransitModes\n\t\t\t\t\t\tbind:directModes\n\t\t\t\t\t\tbind:elevationCosts\n\t\t\t\t\t\tbind:maxPreTransitTime\n\t\t\t\t\t\tbind:maxPostTransitTime\n\t\t\t\t\t\tbind:maxDirectTime\n\t\t\t\t\t\tbind:ignorePreTransitRentalReturnConstraints\n\t\t\t\t\t\tbind:ignorePostTransitRentalReturnConstraints\n\t\t\t\t\t\tbind:ignoreDirectRentalReturnConstraints\n\t\t\t\t\t\tbind:preTransitProviderGroups\n\t\t\t\t\t\tbind:postTransitProviderGroups\n\t\t\t\t\t\tbind:directProviderGroups\n\t\t\t\t\t\tbind:via\n\t\t\t\t\t\tbind:viaMinimumStay\n\t\t\t\t\t\tbind:viaLabels\n\t\t\t\t\t\t{hasDebug}\n\t\t\t\t\t/>\n\t\t\t\t</Card>\n\t\t\t</Tabs.Content>\n\t\t\t<Tabs.Content value=\"departures\">\n\t\t\t\t<Card class=\"overflow-y-auto overflow-x-hidden bg-background rounded-lg\">\n\t\t\t\t\t<DeparturesMask bind:time />\n\t\t\t\t</Card>\n\t\t\t</Tabs.Content>\n\t\t\t<Tabs.Content value=\"isochrones\">\n\t\t\t\t<Card class=\"overflow-y-auto overflow-x-hidden bg-background rounded-lg\">\n\t\t\t\t\t<IsochronesMask\n\t\t\t\t\t\tbind:one\n\t\t\t\t\t\t{serverConfig}\n\t\t\t\t\t\tbind:maxTravelTime\n\t\t\t\t\t\tgeocodingBiasPlace={center}\n\t\t\t\t\t\tbind:time\n\t\t\t\t\t\tbind:useRoutedTransfers\n\t\t\t\t\t\tbind:pedestrianProfile\n\t\t\t\t\t\tbind:requireCarTransport\n\t\t\t\t\t\tbind:requireBikeTransport\n\t\t\t\t\t\tbind:transitModes\n\t\t\t\t\t\tbind:maxTransfers\n\t\t\t\t\t\tbind:preTransitModes\n\t\t\t\t\t\tbind:postTransitModes\n\t\t\t\t\t\tbind:maxPreTransitTime\n\t\t\t\t\t\tbind:maxPostTransitTime\n\t\t\t\t\t\tbind:arriveBy\n\t\t\t\t\t\tbind:elevationCosts\n\t\t\t\t\t\tbind:ignorePreTransitRentalReturnConstraints\n\t\t\t\t\t\tbind:ignorePostTransitRentalReturnConstraints\n\t\t\t\t\t\tbind:options={isochronesOptions}\n\t\t\t\t\t\tbind:preTransitProviderGroups\n\t\t\t\t\t\tbind:postTransitProviderGroups\n\t\t\t\t\t\tbind:directProviderGroups\n\t\t\t\t\t\t{hasDebug}\n\t\t\t\t\t/>\n\t\t\t\t</Card>\n\t\t\t</Tabs.Content>\n\t\t</Tabs.Root>\n\t</Control>\n\n\t{#if activeTab == 'connections' && routingResponses.length !== 0 && !page.state.selectedItinerary}\n\t\t<Control class=\"min-h-0 md:flex md:flex-col md:mb-2} \">\n\t\t\t<Card\n\t\t\t\tclass=\"scrollable w-[520px] h-full md:h-[70vh] {isSmallScreen\n\t\t\t\t\t? 'border-0 shadow-none'\n\t\t\t\t\t: ''} overflow-x-hidden bg-background rounded-lg mb-2\"\n\t\t\t>\n\t\t\t\t<ItineraryList\n\t\t\t\t\t{baseResponse}\n\t\t\t\t\t{routingResponses}\n\t\t\t\t\t{baseQuery}\n\t\t\t\t\tselectItinerary={(selectedItinerary) => {\n\t\t\t\t\t\tpushState('', {\n\t\t\t\t\t\t\tselectedItinerary: selectedItinerary,\n\t\t\t\t\t\t\tscrollY: undefined,\n\t\t\t\t\t\t\tactiveTab: 'connections'\n\t\t\t\t\t\t});\n\t\t\t\t\t}}\n\t\t\t\t\tupdateStartDest={preprocessItinerary(from, to)}\n\t\t\t\t/>\n\t\t\t</Card>\n\t\t</Control>\n\t\t{#if showMap && !page.state.selectedItinerary}\n\t\t\t{#each routingResponses as r, rI (rI)}\n\t\t\t\t{#await r then r}\n\t\t\t\t\t{#each r.itineraries as it, i (i)}\n\t\t\t\t\t\t<ItineraryGeoJson\n\t\t\t\t\t\t\titinerary={it}\n\t\t\t\t\t\t\tid=\"{rI}-{i}\"\n\t\t\t\t\t\t\tselected={false}\n\t\t\t\t\t\t\tselectItinerary={() => {\n\t\t\t\t\t\t\t\tpushState('', {\n\t\t\t\t\t\t\t\t\tselectedItinerary: it,\n\t\t\t\t\t\t\t\t\tactiveTab: 'connections'\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t{level}\n\t\t\t\t\t\t\t{theme}\n\t\t\t\t\t\t/>\n\t\t\t\t\t{/each}\n\t\t\t\t{/await}\n\t\t\t{/each}\n\t\t{/if}\n\t{/if}\n\n\t{#if activeTab == 'connections' && page.state.selectedItinerary}\n\t\t<Control class=\"min-h-0 md:mb-2 md:flex\">\n\t\t\t<Card class=\"w-[520px] bg-background rounded-lg  flex flex-col mb-2\">\n\t\t\t\t<div class=\"w-full flex justify-between items-center shadow-md pl-1 mb-1\">\n\t\t\t\t\t<h2 class=\"ml-2 text-base font-semibold\">{t.journeyDetails}</h2>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\thistory.back();\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<X />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclass={'p-2 md:p-4 overflow-y-auto overflow-x-hidden min-h-0 ' +\n\t\t\t\t\t\t(showMap ? 'md:max-h-[60vh]' : '')}\n\t\t\t\t>\n\t\t\t\t\t<ConnectionDetail itinerary={page.state.selectedItinerary} />\n\t\t\t\t</div>\n\t\t\t</Card>\n\t\t</Control>\n\t\t{#if showMap}\n\t\t\t<ItineraryGeoJson itinerary={page.state.selectedItinerary} selected={true} {level} {theme} />\n\t\t\t<StopGeoJSON itinerary={page.state.selectedItinerary} {theme} />\n\t\t{/if}\n\t{/if}\n\n\t{#if activeTab == 'departures' && page.state.selectedStop}\n\t\t<Control class=\"min-h-0 md:mb-2\">\n\t\t\t<Card class=\"w-[520px] md:max-h-[60vh] h-full bg-background rounded-lg flex flex-col mb-2\">\n\t\t\t\t<div class=\"w-full flex justify-between items-center shadow-md pl-1 mb-1\">\n\t\t\t\t\t<h2 class=\"ml-2 text-base font-semibold\">\n\t\t\t\t\t\t{#if page.state.stopArriveBy}\n\t\t\t\t\t\t\t{t.arrivals}\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t{t.departures}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t\tin\n\t\t\t\t\t\t{stopNameFromResponse}\n\t\t\t\t\t</h2>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\thistory.back();\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<X />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"p-2 md:p-4 overflow-y-auto overflow-x-hidden min-h-0 md:max-h-[60vh]\">\n\t\t\t\t\t<StopTimes\n\t\t\t\t\t\tstopId={page.state.selectedStop.stopId}\n\t\t\t\t\t\tstopName={page.state.selectedStop.name}\n\t\t\t\t\t\ttime={page.state.selectedStop.time}\n\t\t\t\t\t\tbind:stop\n\t\t\t\t\t\tbind:stopMarker\n\t\t\t\t\t\tbind:stopNameFromResponse\n\t\t\t\t\t\tarriveBy={page.state.stopArriveBy}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</Card>\n\t\t</Control>\n\t{/if}\n\n\t{#if activeTab == 'isochrones' && one.match}\n\t\t<Control class=\"min-h-0 md:mb-2 {isochronesOptions.status == 'DONE' ? 'hide' : ''}\">\n\t\t\t<Card class=\"w-[520px] overflow-y-auto overflow-x-hidden bg-background rounded-lg\">\n\t\t\t\t<IsochronesInfo options={isochronesOptions} />\n\t\t\t</Card>\n\t\t</Control>\n\t{/if}\n{/snippet}\n{#if dataLoaded}\n\t<Map\n\t\tbind:map\n\t\tbind:bounds\n\t\tbind:zoom\n\t\tbind:center\n\t\tclass={cn('h-dvh pt-2 overflow-clip', theme)}\n\t\tstyle={showMap ? style : undefined}\n\t\tattribution={false}\n\t>\n\t\t{#if hasDebug}\n\t\t\t<Control position=\"top-right\" class=\"text-right\">\n\t\t\t\t<Debug {bounds} {level} {zoom} />\n\t\t\t\t<Button\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tvariant={showRoutes ? 'default' : 'outline'}\n\t\t\t\t\tonclick={toggleRoutesOverlay}\n\t\t\t\t>\n\t\t\t\t\t<Waypoints class=\"w-5 h-5\" />\n\t\t\t\t</Button>\n\t\t\t</Control>\n\t\t{/if}\n\n\t\t<Control position=\"top-right\" class=\"text-right\">\n\t\t\t<Debug {bounds} {level} {zoom} />\n\t\t\t<Button\n\t\t\t\tsize=\"icon\"\n\t\t\t\tvariant={withHillshades ? 'default' : 'outline'}\n\t\t\t\tonclick={() => (withHillshades = !withHillshades)}\n\t\t\t>\n\t\t\t\t<MountainSnow class=\"w-5 h-5\" />\n\t\t\t</Button>\n\t\t</Control>\n\n\t\t<LevelSelect {bounds} {zoom} bind:level />\n\n\t\t{#if browser}\n\t\t\t{#if isSmallScreen}\n\t\t\t\t<Drawer class=\"relative z-10 h-full mt-3 flex flex-col\" bind:showMap>\n\t\t\t\t\t{@render resultContent()}\n\t\t\t\t</Drawer>\n\t\t\t{:else}\n\t\t\t\t<div class=\"maplibregl-ctrl-top-left flex flex-col max-h-[97vh]\">\n\t\t\t\t\t{@render resultContent()}\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t{/if}\n\n\t\t<div class=\"maplibregl-ctrl-{isSmallScreen ? 'top-left' : 'bottom-right'}\">\n\t\t\t<div class=\"maplibregl-ctrl maplibregl-ctrl-attrib\">\n\t\t\t\t<div class=\"maplibregl-ctrl-attrib-inner\">\n\t\t\t\t\t&copy; <a href=\"http://www.openstreetmap.org/copyright\" target=\"_blank\">OpenStreetMap</a>\n\t\t\t\t\t{#if dataAttributionLink}\n\t\t\t\t\t\t| <a href={dataAttributionLink} target=\"_blank\">{t.timetableSources}</a>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t{#if showMap}\n\t\t\t{#if activeTab != 'isochrones'}\n\t\t\t\t<Control position=\"top-right\" class=\"pb-4 text-right\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tonclick={() => {\n\t\t\t\t\t\t\tcolorMode = (function () {\n\t\t\t\t\t\t\t\tswitch (colorMode) {\n\t\t\t\t\t\t\t\t\tcase 'rt':\n\t\t\t\t\t\t\t\t\t\treturn 'route';\n\t\t\t\t\t\t\t\t\tcase 'route':\n\t\t\t\t\t\t\t\t\t\treturn 'mode';\n\t\t\t\t\t\t\t\t\tcase 'mode':\n\t\t\t\t\t\t\t\t\t\treturn 'none';\n\t\t\t\t\t\t\t\t\tcase 'none':\n\t\t\t\t\t\t\t\t\t\treturn 'rt';\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t})();\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if colorMode == 'rt'}\n\t\t\t\t\t\t\t<Rss class=\"h-[1.2rem] w-[1.2rem]\" />\n\t\t\t\t\t\t{:else if colorMode == 'mode'}\n\t\t\t\t\t\t\t<TrainFront class=\"h-[1.2rem] w-[1.2rem]\" />\n\t\t\t\t\t\t{:else if colorMode == 'none'}\n\t\t\t\t\t\t\t<Ban class=\"h-[1.2rem] w-[1.2rem]\" />\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t<Palette class=\"h-[1.2rem] w-[1.2rem]\" />\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button size=\"icon\" onclick={() => getLocation()}>\n\t\t\t\t\t\t<LocateFixed class=\"w-5 h-5\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</Control>\n\t\t\t\t{#if showRoutes}\n\t\t\t\t\t{#key routesOverlaySession}\n\t\t\t\t\t\t<Routes\n\t\t\t\t\t\t\t{map}\n\t\t\t\t\t\t\t{bounds}\n\t\t\t\t\t\t\t{zoom}\n\t\t\t\t\t\t\tshapesDebugEnabled={serverConfig?.shapesDebugEnabled === true}\n\t\t\t\t\t\t/>\n\t\t\t\t\t{/key}\n\t\t\t\t{/if}\n\t\t\t\t<Rentals {map} {bounds} {zoom} {theme} debug={hasDebug} />\n\t\t\t{/if}\n\n\t\t\t<RailViz {map} {bounds} {zoom} {colorMode} />\n\t\t\t<Isochrones\n\t\t\t\t{map}\n\t\t\t\t{bounds}\n\t\t\t\t{isochronesData}\n\t\t\t\tstreetModes={arriveBy ? preTransitModes : postTransitModes}\n\t\t\t\twheelchair={pedestrianProfile === 'WHEELCHAIR'}\n\t\t\t\tmaxAllTime={arriveBy ? maxPreTransitTime : maxPostTransitTime}\n\t\t\t\tcircleResolution={isochronesCircleResolution}\n\t\t\t\tactive={activeTab == 'isochrones'}\n\t\t\t\tbind:options={isochronesOptions}\n\t\t\t/>\n\n\t\t\t<Popup trigger=\"contextmenu\" children={contextMenu} />\n\n\t\t\t{#if from && activeTab == 'connections'}\n\t\t\t\t<Marker\n\t\t\t\t\tcolor=\"green\"\n\t\t\t\t\tdraggable={true}\n\t\t\t\t\t{level}\n\t\t\t\t\tbind:location={from}\n\t\t\t\t\tbind:marker={fromMarker}\n\t\t\t\t/>\n\t\t\t{/if}\n\n\t\t\t{#if stop && activeTab == 'departures'}\n\t\t\t\t<Marker\n\t\t\t\t\tcolor=\"black\"\n\t\t\t\t\tdraggable={false}\n\t\t\t\t\t{level}\n\t\t\t\t\tbind:location={stop}\n\t\t\t\t\tbind:marker={stopMarker}\n\t\t\t\t/>\n\t\t\t{/if}\n\n\t\t\t{#if to && activeTab == 'connections'}\n\t\t\t\t<Marker color=\"red\" draggable={true} {level} bind:location={to} bind:marker={toMarker} />\n\t\t\t{/if}\n\n\t\t\t{#if one && activeTab == 'isochrones'}\n\t\t\t\t<Marker\n\t\t\t\t\tcolor=\"yellow\"\n\t\t\t\t\tdraggable={true}\n\t\t\t\t\t{level}\n\t\t\t\t\tbind:location={one}\n\t\t\t\t\tbind:marker={oneMarker}\n\t\t\t\t/>\n\t\t\t{/if}\n\t\t{/if}\n\t</Map>\n{/if}\n"
  },
  {
    "path": "ui/static/sprite_sdf.json",
    "content": "{\n  \"elevator\": {\n    \"height\": 26,\n    \"pixelRatio\": 1,\n    \"width\": 26,\n    \"x\": 0,\n    \"y\": 0,\n    \"sdf\": true\n  }\n}"
  },
  {
    "path": "ui/static/sprite_sdf@2x.json",
    "content": "{\n  \"elevator\": {\n    \"height\": 52,\n    \"pixelRatio\": 2,\n    \"width\": 52,\n    \"x\": 0,\n    \"y\": 0,\n    \"sdf\": true\n  }\n}"
  },
  {
    "path": "ui/svelte.config.js",
    "content": "import adapter from '@sveltejs/adapter-static';\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\t// Consult https://kit.svelte.dev/docs/integrations#preprocessors\n\t// for more information about preprocessors\n\tpreprocess: vitePreprocess(),\n\n\tkit: {\n\t\tinlineStyleThreshold: 5000,\n\t\tadapter: adapter({\n\t\t\t// default options are shown. On some platforms\n\t\t\t// these options are set automatically — see below\n\t\t\tpages: 'build',\n\t\t\tassets: 'build',\n\t\t\tfallback: undefined,\n\t\t\tprecompress: false,\n\t\t\tstrict: true\n\t\t})\n\t}\n};\n\nexport default config;\n"
  },
  {
    "path": "ui/tailwind.config.js",
    "content": "import { fontFamily } from 'tailwindcss/defaultTheme';\nimport tailwindcssAnimate from 'tailwindcss-animate';\n\n/** @type {import('tailwindcss').Config} */\nconst config = {\n\tdarkMode: ['class'],\n\tcontent: ['./src/**/*.{html,js,svelte,ts}'],\n\tsafelist: ['dark'],\n\ttheme: {\n\t\tcontainer: {\n\t\t\tcenter: true,\n\t\t\tpadding: '2rem',\n\t\t\tscreens: {\n\t\t\t\t'2xl': '1400px'\n\t\t\t}\n\t\t},\n\t\textend: {\n\t\t\tcolors: {\n\t\t\t\tborder: 'hsl(var(--border) / <alpha-value>)',\n\t\t\t\tinput: 'hsl(var(--input) / <alpha-value>)',\n\t\t\t\tring: 'hsl(var(--ring) / <alpha-value>)',\n\t\t\t\tbackground: 'hsl(var(--background) / <alpha-value>)',\n\t\t\t\tforeground: 'hsl(var(--foreground) / <alpha-value>)',\n\t\t\t\tprimary: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--primary) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--primary-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\tsecondary: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--secondary) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--secondary-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\tdestructive: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--destructive) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--destructive-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\tmuted: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--muted) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--muted-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\taccent: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--accent) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--accent-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\tpopover: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--popover) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--popover-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\tcard: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--card) / <alpha-value>)',\n\t\t\t\t\tforeground: 'hsl(var(--card-foreground) / <alpha-value>)'\n\t\t\t\t},\n\t\t\t\tsidebar: {\n\t\t\t\t\tDEFAULT: 'hsl(var(--sidebar-background))',\n\t\t\t\t\tforeground: 'hsl(var(--sidebar-foreground))',\n\t\t\t\t\tprimary: 'hsl(var(--sidebar-primary))',\n\t\t\t\t\t'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',\n\t\t\t\t\taccent: 'hsl(var(--sidebar-accent))',\n\t\t\t\t\t'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',\n\t\t\t\t\tborder: 'hsl(var(--sidebar-border))',\n\t\t\t\t\tring: 'hsl(var(--sidebar-ring))'\n\t\t\t\t}\n\t\t\t},\n\t\t\tborderRadius: {\n\t\t\t\txl: 'calc(var(--radius) + 4px)',\n\t\t\t\tlg: 'var(--radius)',\n\t\t\t\tmd: 'calc(var(--radius) - 2px)',\n\t\t\t\tsm: 'calc(var(--radius) - 4px)'\n\t\t\t},\n\t\t\tfontFamily: {\n\t\t\t\tsans: [...fontFamily.sans]\n\t\t\t},\n\t\t\tkeyframes: {\n\t\t\t\t'accordion-down': {\n\t\t\t\t\tfrom: { height: '0' },\n\t\t\t\t\tto: { height: 'var(--bits-accordion-content-height)' }\n\t\t\t\t},\n\t\t\t\t'accordion-up': {\n\t\t\t\t\tfrom: { height: 'var(--bits-accordion-content-height)' },\n\t\t\t\t\tto: { height: '0' }\n\t\t\t\t},\n\t\t\t\t'caret-blink': {\n\t\t\t\t\t'0%,70%,100%': { opacity: '1' },\n\t\t\t\t\t'20%,50%': { opacity: '0' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tanimation: {\n\t\t\t\t'accordion-down': 'accordion-down 0.2s ease-out',\n\t\t\t\t'accordion-up': 'accordion-up 0.2s ease-out',\n\t\t\t\t'caret-blink': 'caret-blink 1.25s ease-out infinite'\n\t\t\t}\n\t\t}\n\t},\n\tplugins: [tailwindcssAnimate]\n};\n\nexport default config;\n"
  },
  {
    "path": "ui/tests/test.ts",
    "content": "import { expect, test } from '@playwright/test';\n\ntest('index page has expected h1', async ({ page }) => {\n\tawait page.goto('/');\n\tawait expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();\n});\n"
  },
  {
    "path": "ui/tsconfig.json",
    "content": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t},\n\t\"exclude\": [\n\t\t\"../node_modules/**\",\n\t\t\"../src/service-worker.js\",\n\t\t\"../src/service-worker/**/*.js\",\n\t\t\"../src/service-worker.ts\",\n\t\t\"../src/service-worker/**/*.ts\",\n\t\t\"../src/service-worker.d.ts\",\n\t\t\"../src/service-worker/**/*.d.ts\",\n\t\t\"api/dist/**\",\n\t\t\"api/node_modules/**\"\n\t]\n\t// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias\n\t// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files\n\t//\n\t// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes\n\t// from the referenced tsconfig.json - TypeScript does not merge them in\n}\n"
  },
  {
    "path": "ui/vite.config.ts",
    "content": "import { sveltekit } from '@sveltejs/kit/vite';\nimport { defineConfig } from 'vitest/config';\nexport default defineConfig({\n\tplugins: [sveltekit()],\n\ttest: {\n\t\tinclude: ['src/**/*.{test,spec}.{js,ts}']\n\t},\n\tserver: {\n\t\tfs: { strict: false }\n\t},\n\tbuild: {\n\t\tsourcemap: true,\n\t\trollupOptions: {\n\t\t\toutput: {\n\t\t\t\tmanualChunks: (id) => {\n\t\t\t\t\tif (id.includes('node_modules')) {\n\t\t\t\t\t\tif (id.includes('deck.gl')) return 'deck-vendor';\n\t\t\t\t\t\tif (id.includes('svelte')) return 'svelte-vendor';\n\t\t\t\t\t\tif (id.includes('luma.gl')) return 'luma-vendor';\n\t\t\t\t\t\tif (id.includes('.gl')) return 'gl-vendor';\n\t\t\t\t\t\treturn 'other-vendor';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n});\n"
  }
]