feat(train): search for places in the route planner, through Photon (ADR 0030, D50)
A `search` endpoint asks the instance's Photon (compose.search.yaml, Europe's database from GraphHopper, renewed monthly) for people signed in, limited and unlogged. The planner gets a field over its map: the places found nearest the map's centre first, in the app's language; one chosen is the next waypoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
190c3872f4
commit
edd2df79bd
4 changed files with 2184 additions and 7 deletions
|
|
@ -24,6 +24,8 @@ import 'package:halkyon_train_client/src/protocol/routing/route_point.dart'
|
|||
as _iq1flhug;
|
||||
import 'package:halkyon_train_client/src/protocol/routing/route_profile.dart'
|
||||
as _isvxstnj;
|
||||
import 'package:halkyon_train_client/src/protocol/search/place.dart'
|
||||
as _inm997sc;
|
||||
import 'package:http/http.dart' as _i85jenna;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
|
|
@ -139,6 +141,44 @@ class EndpointRouting extends _isc.EndpointRef {
|
|||
);
|
||||
}
|
||||
|
||||
/// Places found by the instance's Photon (ADR 0030, D50), for those signed
|
||||
/// in: Photon has no sign-in of its own, so the server alone reaches it.
|
||||
/// Neither the query nor the places are kept or logged. Refusals are
|
||||
/// `SearchRefused`.
|
||||
/// {@category Endpoint}
|
||||
class EndpointSearch extends _isc.EndpointRef {
|
||||
EndpointSearch(_isc.EndpointCaller caller) : super(caller);
|
||||
|
||||
@override
|
||||
String get name => 'search';
|
||||
|
||||
/// Whether the instance searches for places.
|
||||
_ida.Future<bool> offered() => caller.callServerEndpoint<bool>(
|
||||
'search',
|
||||
'offered',
|
||||
{},
|
||||
);
|
||||
|
||||
/// The places [query] finds, nearest [near] first (the map's centre),
|
||||
/// named in [language] (`en`, `de`; else each its own name). Refused where
|
||||
/// the instance has no Photon or it fails, for a query of fewer than
|
||||
/// `PlaceSearch.minQuery` or more than `PlaceSearch.maxQuery` characters,
|
||||
/// and beyond `PlaceSearch.perMinute` searches a minute.
|
||||
_ida.Future<List<_inm997sc.Place>> places(
|
||||
String query, {
|
||||
_iq1flhug.RoutePoint? near,
|
||||
String? language,
|
||||
}) => caller.callServerEndpoint<List<_inm997sc.Place>>(
|
||||
'search',
|
||||
'places',
|
||||
{
|
||||
'query': query,
|
||||
'near': near,
|
||||
'language': language,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class Modules {
|
||||
Modules(Client client) {
|
||||
base = _ihk4p5t7.Caller(client);
|
||||
|
|
@ -189,6 +229,7 @@ class Client extends _isc.ServerpodClientShared {
|
|||
firmware = EndpointFirmware(this);
|
||||
map = EndpointMap(this);
|
||||
routing = EndpointRouting(this);
|
||||
search = EndpointSearch(this);
|
||||
modules = Modules(this);
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +239,8 @@ class Client extends _isc.ServerpodClientShared {
|
|||
|
||||
late final EndpointRouting routing;
|
||||
|
||||
late final EndpointSearch search;
|
||||
|
||||
late final Modules modules;
|
||||
|
||||
@override
|
||||
|
|
@ -205,6 +248,7 @@ class Client extends _isc.ServerpodClientShared {
|
|||
'firmware': firmware,
|
||||
'map': map,
|
||||
'routing': routing,
|
||||
'search': search,
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import 'package:halkyon_base_client/halkyon_base_client.dart' as _ihk4p5t7;
|
|||
import 'package:halkyon_oidc_client/halkyon_oidc_client.dart' as _ibkpwzgl;
|
||||
import 'package:halkyon_train_client/src/protocol/routing/route_point.dart'
|
||||
as _iq1flhug;
|
||||
import 'package:halkyon_train_client/src/protocol/search/place.dart'
|
||||
as _inm997sc;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
||||
|
|
@ -39,6 +41,8 @@ import 'routing/route_point.dart' as _isqtfus6;
|
|||
import 'routing/route_profile.dart' as _ifl4j5ep;
|
||||
import 'routing/route_refused.dart' as _ix4aepwk;
|
||||
import 'routing/route_stretch.dart' as _ic7f85bc;
|
||||
import 'search/place.dart' as _iadh29yw;
|
||||
import 'search/search_refused.dart' as _ihbxkwke;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
import 'workout/workout_series.dart' as _i92mefto;
|
||||
export 'day/day.dart';
|
||||
|
|
@ -57,6 +61,8 @@ export 'routing/route_point.dart';
|
|||
export 'routing/route_profile.dart';
|
||||
export 'routing/route_refused.dart';
|
||||
export 'routing/route_stretch.dart';
|
||||
export 'search/place.dart';
|
||||
export 'search/search_refused.dart';
|
||||
export 'workout/workout.dart';
|
||||
export 'workout/workout_series.dart';
|
||||
export 'client.dart';
|
||||
|
|
@ -824,6 +830,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _ic7f85bc.RouteStretch) {
|
||||
return _ic7f85bc.RouteStretch.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iadh29yw.Place) {
|
||||
return _iadh29yw.Place.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ihbxkwke.SearchRefused) {
|
||||
return _ihbxkwke.SearchRefused.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i5b9b17z.Workout) {
|
||||
return _i5b9b17z.Workout.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -881,6 +893,13 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _isc.getType<_ic7f85bc.RouteStretch?>()) {
|
||||
return (data != null ? _ic7f85bc.RouteStretch.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_iadh29yw.Place?>()) {
|
||||
return (data != null ? _iadh29yw.Place.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_ihbxkwke.SearchRefused?>()) {
|
||||
return (data != null ? _ihbxkwke.SearchRefused.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _isc.getType<_i5b9b17z.Workout?>()) {
|
||||
return (data != null ? _i5b9b17z.Workout.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -915,6 +934,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_inm997sc.Place>) {
|
||||
return (data as List).map((e) => deserialize<_inm997sc.Place>(e)).toList()
|
||||
as T;
|
||||
}
|
||||
try {
|
||||
return _ihk4p5t7.Protocol().deserialize<T>(data, t);
|
||||
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
||||
|
|
@ -951,6 +974,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
_ifl4j5ep.RouteProfile => 'RouteProfile',
|
||||
_ix4aepwk.RouteRefused => 'RouteRefused',
|
||||
_ic7f85bc.RouteStretch => 'RouteStretch',
|
||||
_iadh29yw.Place => 'Place',
|
||||
_ihbxkwke.SearchRefused => 'SearchRefused',
|
||||
_i5b9b17z.Workout => 'Workout',
|
||||
_i92mefto.WorkoutSeries => 'WorkoutSeries',
|
||||
_ => null,
|
||||
|
|
@ -1002,6 +1027,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return 'RouteRefused';
|
||||
case _ic7f85bc.RouteStretch():
|
||||
return 'RouteStretch';
|
||||
case _iadh29yw.Place():
|
||||
return 'Place';
|
||||
case _ihbxkwke.SearchRefused():
|
||||
return 'SearchRefused';
|
||||
case _i5b9b17z.Workout():
|
||||
return 'Workout';
|
||||
case _i92mefto.WorkoutSeries():
|
||||
|
|
@ -1090,6 +1119,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (dataClassName == 'RouteStretch') {
|
||||
return deserialize<_ic7f85bc.RouteStretch>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Place') {
|
||||
return deserialize<_iadh29yw.Place>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'SearchRefused') {
|
||||
return deserialize<_ihbxkwke.SearchRefused>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Workout') {
|
||||
return deserialize<_i5b9b17z.Workout>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// A place found by the instance's search (ADR 0030, D50): what the planner
|
||||
/// lists and makes a waypoint of.
|
||||
abstract class Place
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
Place._({
|
||||
required this.name,
|
||||
this.detail,
|
||||
required this.kind,
|
||||
required this.lat,
|
||||
required this.lon,
|
||||
});
|
||||
|
||||
factory Place({
|
||||
required String name,
|
||||
String? detail,
|
||||
required String kind,
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) = _PlaceImpl;
|
||||
|
||||
factory Place.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return Place(
|
||||
name: jsonSerialization['name'] as String,
|
||||
detail: jsonSerialization['detail'] as String?,
|
||||
kind: jsonSerialization['kind'] as String,
|
||||
lat: (jsonSerialization['lat'] as num).toDouble(),
|
||||
lon: (jsonSerialization['lon'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Its name, or its street and number for an address.
|
||||
String name;
|
||||
|
||||
/// Where it is, as the list shows under the name: its street, its town and
|
||||
/// its country, those the name doesn't say already; null where none.
|
||||
String? detail;
|
||||
|
||||
/// What kind of place, as Photon names it (`city`, `street`, `house`,
|
||||
/// `locality`, `other`, …).
|
||||
String kind;
|
||||
|
||||
double lat;
|
||||
|
||||
double lon;
|
||||
|
||||
/// Returns a shallow copy of this [Place]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
Place copyWith({
|
||||
String? name,
|
||||
String? detail,
|
||||
String? kind,
|
||||
double? lat,
|
||||
double? lon,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'Place',
|
||||
'name': name,
|
||||
if (detail != null) 'detail': detail,
|
||||
'kind': kind,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'Place',
|
||||
'name': name,
|
||||
if (detail != null) 'detail': detail,
|
||||
'kind': kind,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PlaceImpl extends Place {
|
||||
_PlaceImpl({
|
||||
required String name,
|
||||
String? detail,
|
||||
required String kind,
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) : super._(
|
||||
name: name,
|
||||
detail: detail,
|
||||
kind: kind,
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [Place]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
Place copyWith({
|
||||
String? name,
|
||||
Object? detail = _Undefined,
|
||||
String? kind,
|
||||
double? lat,
|
||||
double? lon,
|
||||
}) {
|
||||
return Place(
|
||||
name: name ?? this.name,
|
||||
detail: detail is String? ? detail : this.detail,
|
||||
kind: kind ?? this.kind,
|
||||
lat: lat ?? this.lat,
|
||||
lon: lon ?? this.lon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
|
||||
/// Why the instance searches nothing (ADR 0030, D50): `off` where it has no
|
||||
/// Photon, `invalid` for a query shorter than `PlaceSearch.minQuery` or
|
||||
/// longer than `PlaceSearch.maxQuery` characters or a place off the earth,
|
||||
/// `tooOften` beyond `PlaceSearch.perMinute` searches a minute,
|
||||
/// `unreachable` where Photon didn't answer.
|
||||
abstract class SearchRefused
|
||||
implements
|
||||
_isc.SerializableException,
|
||||
_isc.SerializableModel,
|
||||
_isc.ProtocolSerialization {
|
||||
SearchRefused._({required this.reason});
|
||||
|
||||
factory SearchRefused({required String reason}) = _SearchRefusedImpl;
|
||||
|
||||
factory SearchRefused.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return SearchRefused(reason: jsonSerialization['reason'] as String);
|
||||
}
|
||||
|
||||
String reason;
|
||||
|
||||
/// Returns a shallow copy of this [SearchRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
SearchRefused copyWith({String? reason});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'SearchRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'SearchRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SearchRefused(reason: $reason)';
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchRefusedImpl extends SearchRefused {
|
||||
_SearchRefusedImpl({required String reason}) : super._(reason: reason);
|
||||
|
||||
/// Returns a shallow copy of this [SearchRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
SearchRefused copyWith({String? reason}) {
|
||||
return SearchRefused(reason: reason ?? this.reason);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue