Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions include/motis/endpoints/nearest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include "motis-api/motis-api.h"
#include "motis/config.h"

#include "osr/lookup.h"
#include "osr/ways.h"

namespace motis::ep {

struct nearest {
api::NearestResponse operator()(boost::urls::url_view const&) const;

osr::ways const& w_;
osr::lookup const& l_;
config const& c_;
};

} // namespace motis::ep
2 changes: 2 additions & 0 deletions include/motis/motis_instance.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "motis/endpoints/map/trips.h"
#include "motis/endpoints/matches.h"
#include "motis/endpoints/metrics.h"
#include "motis/endpoints/nearest.h"
#include "motis/endpoints/ojp.h"
#include "motis/endpoints/one_to_all.h"
#include "motis/endpoints/one_to_many.h"
Expand Down Expand Up @@ -94,6 +95,7 @@ struct motis_instance {
POST<ep::osr_routing>("/api/route", d);
POST<ep::platforms>("/api/platforms", d);
POST<ep::graph>("/api/graph", d);
GET<ep::nearest>("/nearest/v1", d);
GET<ep::transfers>("/api/debug/transfers", d);
GET<ep::flex_locations>("/api/debug/flex", d);
GET<ep::levels>("/api/v1/map/levels", d);
Expand Down
70 changes: 69 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3077,7 +3077,45 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HealthResponse'


/nearest/v1/{profile}/{coordinates}:
get:
operationId: nearest
parameters:
- name: profile
in: path
required: true
schema:
type: string

- name: coordinates
in: path
required: true
schema:
type: string

- name: number
in: query
required: false
description: Number of nearest segments that should be returned.
schema:
type: integer
default: 1

- name: radiuses
in: query
required: false
description: Radius constraint
schema:
type: number
responses:
'200':
description: Returns the nearest road segment(s) for the provided coordinates.
content:
application/json:
schema:
$ref: '#/components/schemas/NearestResponse'

/api/debug/transfers:
get:
tags:
Expand Down Expand Up @@ -5424,3 +5462,33 @@ components:
gbfs:
type: boolean
description: GBFS feeds.

Waypoint:
type: object
properties:
name:
type: string
description: name Name of the street the coordinate snapped to
location:
type: array
items:
type: number
description: longitude, latitude pair of the snapped coordinate
distance:
type: number
description: The distance of the snapped point from the original

NearestResponse:
type: object
required:
- code
- waypoints
properties:
code:
type: string
message:
type: string
waypoints:
type: array
items:
$ref: '#/components/schemas/Waypoint'
64 changes: 64 additions & 0 deletions src/endpoints/nearest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "motis/endpoints/nearest.h"

#include "motis/parse_location.h"

#include "osr/routing/with_profile.h"

#include "net/bad_request_exception.h"

namespace motis::ep {

api::NearestResponse nearest::operator()(
boost::urls::url_view const& url) const {

api::nearest_params params{url.params(), url.segments()};
osr::search_profile p;

try {
p = osr::to_profile(params.profile_);
} catch (...) {
throw net::bad_request_exception("invalid profile");
}

auto coord = parse_location(params.coordinates_);
utl::verify<net::bad_request_exception>(coord.has_value(),
"invalid coordinates");
utl::verify<net::bad_request_exception>(params.number_ >= 1,
"invalid number");

std::swap(coord->pos_.lat_, coord->pos_.lng_);
api::NearestResponse res;
auto const from = coord.value();
auto const candidates = osr::with_profile(p, [&]<typename P>(P&&) {
constexpr auto kDefaultRadius = 100U;
auto const max_dist = params.radiuses_.value_or(kDefaultRadius);
return l_.match<P>(std::get<typename P::parameters>(osr::get_parameters(p)),
from, false, osr::direction::kForward, max_dist,
nullptr);
});

if (candidates.empty()) {
res.code_ = "NoSegment";
return res;
}

auto const n =
std::min(static_cast<std::size_t>(params.number_), candidates.size());
for (auto i = 0U; i < n; ++i) {
auto const c = candidates[i];
auto const loc = c.closest_point_on_way_;
auto const id = w_.way_names_[c.way_];
auto wp = api::Waypoint{};
if (id != osr::string_idx_t::invalid()) {
wp.name_ = w_.strings_[id].view();
}
wp.distance_ = geo::distance(loc, from.pos_);
wp.location_ = {loc.lng(), loc.lat()};
res.waypoints_.emplace_back(wp);
}

res.code_ = "OK";
return res;
}

} // namespace motis::ep
85 changes: 85 additions & 0 deletions test/endpoints/nearest_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "motis/endpoints/nearest.h"
#include "gtest/gtest.h"

#include "motis/data.h"
#include "motis/import.h"

#include "osr/routing/with_profile.h"

#include "net/bad_request_exception.h"

#include "openapi/bad_request_exception.h"

static std::string query(osr::search_profile const p,
osr::location const loc,
std::size_t const n,
std::size_t const r) {
return fmt::format("/nearest/v1/{}/{},{}?number={}&radiuses={}",
osr::to_str(p), loc.pos_.lng(), loc.pos_.lat(), n, r);
}

TEST(motis, nearest_endpoint_profiles) {
auto ec = std::error_code{};
std::filesystem::remove_all("test/data", ec);
auto const c = motis::config{.osm_ = {"test/resources/test_case.osm.pbf"},
.street_routing_ = true};
motis::import(c, "test/data");
auto d = motis::data{"test/data", c};
auto const ep = motis::ep::nearest{*d.w_, *d.l_, c};
auto const loc = osr::location{{49.87260, 8.63085}, osr::kNoLevel};
auto const radius = 100U;
auto const number = 100U;

for (auto i = 0U; i <= static_cast<uint8_t>(osr::search_profile::kFerry);
++i) {
auto const profile = static_cast<osr::search_profile>(i);
for (auto n = 1U; n <= number; ++n) {
auto const q = query(profile, loc, n, radius);
auto const res = ep(boost::urls::url_view{q});
auto const expected = osr::with_profile(profile, [&]<typename P>(P&&) {
return d.l_->match<P>(
std::get<typename P::parameters>(osr::get_parameters(profile)), loc,
false, osr::direction::kForward, radius, nullptr);
});
if (res.code_ == "NoSegment") {
EXPECT_TRUE(expected.empty());
continue;
}
auto const size = std::min(static_cast<std::size_t>(n), expected.size());
ASSERT_EQ(res.waypoints_.size(), size);
for (auto j = 0U; j < size; ++j) {
EXPECT_EQ((*res.waypoints_[j].location_)[0],
expected[j].closest_point_on_way_.lng());
EXPECT_EQ((*res.waypoints_[j].location_)[1],
expected[j].closest_point_on_way_.lat());
EXPECT_EQ(res.waypoints_[j].distance_,
geo::distance(expected[j].closest_point_on_way_, loc.pos_));
}
}
}
}

TEST(motis, nearest_endpoint_invalid) {
auto ec = std::error_code{};
std::filesystem::remove_all("test/data", ec);
auto const c = motis::config{.osm_ = {"test/resources/test_case.osm.pbf"},
.street_routing_ = true};
motis::import(c, "test/data");
auto d = motis::data{"test/data", c};
auto const ep = motis::ep::nearest{*d.w_, *d.l_, c};

// too few segments
EXPECT_THROW(ep(boost::urls::url_view{"/nearest/v1/foot"}),
openapi::bad_request_exception);

// invalid coordinates
EXPECT_THROW(ep(boost::urls::url_view{"/nearest/v1/foot/abc,def"}),
net::bad_request_exception);

// number < 1
auto const bad_number =
query(osr::search_profile::kFoot,
osr::location{{49.8731904, 8.6221451}, osr::kNoLevel}, 0, 100);
EXPECT_THROW(ep(boost::urls::url_view{bad_number}),
net::bad_request_exception);
}
40 changes: 40 additions & 0 deletions ui/api/openapi/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2528,4 +2528,44 @@ export const HealthResponseSchema = {
description: 'GBFS feeds.'
}
}
} as const;

export const WaypointSchema = {
type: 'object',
properties: {
name: {
type: 'string',
description: 'name Name of the street the coordinate snapped to'
},
location: {
type: 'array',
items: {
type: 'number'
},
description: 'longitude, latitude pair of the snapped coordinate'
},
distance: {
type: 'number',
description: 'The distance of the snapped point from the original'
}
}
} as const;

export const NearestResponseSchema = {
type: 'object',
required: ['code', 'waypoints'],
properties: {
code: {
type: 'string'
},
message: {
type: 'string'
},
waypoints: {
type: 'array',
items: {
'$ref': '#/components/schemas/Waypoint'
}
}
}
} as const;
9 changes: 8 additions & 1 deletion ui/api/openapi/services.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

import { createClient, createConfig, type Options } from '@hey-api/client-fetch';
import type { PlanData, PlanError, PlanResponse, OneToManyData, OneToManyError, OneToManyResponse, OneToManyPostData, OneToManyPostError, OneToManyPostResponse, OneToManyIntermodalData, OneToManyIntermodalError, OneToManyIntermodalResponse2, OneToManyIntermodalPostData, OneToManyIntermodalPostError, OneToManyIntermodalPostResponse, OneToAllData, OneToAllError, OneToAllResponse, ReverseGeocodeData, ReverseGeocodeError, ReverseGeocodeResponse, GeocodeData, GeocodeError, GeocodeResponse, TripData, TripError, TripResponse, RefreshItineraryData, RefreshItineraryError, RefreshItineraryResponse, RefreshItineraryPostData, RefreshItineraryPostError, RefreshItineraryPostResponse, StoptimesData, StoptimesError, StoptimesResponse, TripsData, TripsError, TripsResponse, InitialError, InitialResponse, StopsData, StopsError, StopsResponse, LevelsData, LevelsError, LevelsResponse, RoutesData, RoutesError, RoutesResponse, RouteDetailsData, RouteDetailsError, RouteDetailsResponse, RentalsData, RentalsError, RentalsResponse, HealthError, HealthResponse2, TransfersData, TransfersError, TransfersResponse } from './types.gen';
import type { PlanData, PlanError, PlanResponse, OneToManyData, OneToManyError, OneToManyResponse, OneToManyPostData, OneToManyPostError, OneToManyPostResponse, OneToManyIntermodalData, OneToManyIntermodalError, OneToManyIntermodalResponse2, OneToManyIntermodalPostData, OneToManyIntermodalPostError, OneToManyIntermodalPostResponse, OneToAllData, OneToAllError, OneToAllResponse, ReverseGeocodeData, ReverseGeocodeError, ReverseGeocodeResponse, GeocodeData, GeocodeError, GeocodeResponse, TripData, TripError, TripResponse, RefreshItineraryData, RefreshItineraryError, RefreshItineraryResponse, RefreshItineraryPostData, RefreshItineraryPostError, RefreshItineraryPostResponse, StoptimesData, StoptimesError, StoptimesResponse, TripsData, TripsError, TripsResponse, InitialError, InitialResponse, StopsData, StopsError, StopsResponse, LevelsData, LevelsError, LevelsResponse, RoutesData, RoutesError, RoutesResponse, RouteDetailsData, RouteDetailsError, RouteDetailsResponse, RentalsData, RentalsError, RentalsResponse, HealthError, HealthResponse2, NearestData, NearestError, NearestResponse2, TransfersData, TransfersError, TransfersResponse } from './types.gen';

export const client = createClient(createConfig());

Expand Down Expand Up @@ -256,6 +256,13 @@ export const health = <ThrowOnError extends boolean = false>(options?: Options<u
});
};

export const nearest = <ThrowOnError extends boolean = false>(options: Options<NearestData, ThrowOnError>) => {
return (options?.client ?? client).get<NearestResponse2, NearestError, ThrowOnError>({
...options,
url: '/nearest/v1/{profile}/{coordinates}'
});
};

/**
* Prints all transfers of a timetable location (track, bus stop, etc.)
*/
Expand Down
42 changes: 42 additions & 0 deletions ui/api/openapi/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,27 @@ export type HealthResponse = {
gbfs?: boolean;
};

export type Waypoint = {
/**
* name Name of the street the coordinate snapped to
*/
name?: string;
/**
* longitude, latitude pair of the snapped coordinate
*/
location?: Array<(number)>;
/**
* The distance of the snapped point from the original
*/
distance?: number;
};

export type NearestResponse = {
code: string;
message?: string;
waypoints: Array<Waypoint>;
};

export type PlanData = {
query: {
/**
Expand Down Expand Up @@ -3581,6 +3602,27 @@ export type HealthResponse2 = (HealthResponse);

export type HealthError = (HealthResponse);

export type NearestData = {
path: {
coordinates: string;
profile: string;
};
query?: {
/**
* Number of nearest segments that should be returned.
*/
number?: number;
/**
* Radius constraint
*/
radiuses?: number;
};
};

export type NearestResponse2 = (NearestResponse);

export type NearestError = unknown;

export type TransfersData = {
query: {
/**
Expand Down
Loading