mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	feat(server): reverse geocoding endpoint (#11430)
* feat(server): reverse geocoding endpoint * chore: rename error message
This commit is contained in:
		
							parent
							
								
									a70cd368af
								
							
						
					
					
						commit
						ebc71e428d
					
				| @ -159,4 +159,75 @@ describe('/map', () => { | |||||||
|       expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); |       expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   describe('GET /map/reverse-geocode', () => { | ||||||
|  |     it('should require authentication', async () => { | ||||||
|  |       const { status, body } = await request(app).get('/map/reverse-geocode'); | ||||||
|  |       expect(status).toBe(401); | ||||||
|  |       expect(body).toEqual(errorDto.unauthorized); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should throw an error if a lat is not provided', async () => { | ||||||
|  |       const { status, body } = await request(app) | ||||||
|  |         .get('/map/reverse-geocode?lon=123') | ||||||
|  |         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||||
|  |       expect(status).toBe(400); | ||||||
|  |       expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should throw an error if a lat is not a number', async () => { | ||||||
|  |       const { status, body } = await request(app) | ||||||
|  |         .get('/map/reverse-geocode?lat=abc&lon=123.456') | ||||||
|  |         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||||
|  |       expect(status).toBe(400); | ||||||
|  |       expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should throw an error if a lat is out of range', async () => { | ||||||
|  |       const { status, body } = await request(app) | ||||||
|  |         .get('/map/reverse-geocode?lat=91&lon=123.456') | ||||||
|  |         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||||
|  |       expect(status).toBe(400); | ||||||
|  |       expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should throw an error if a lon is not provided', async () => { | ||||||
|  |       const { status, body } = await request(app) | ||||||
|  |         .get('/map/reverse-geocode?lat=75') | ||||||
|  |         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||||
|  |       expect(status).toBe(400); | ||||||
|  |       expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180'])); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const reverseGeocodeTestCases = [ | ||||||
|  |       { | ||||||
|  |         name: 'Vaucluse', | ||||||
|  |         lat: -33.858_977_058_663_13, | ||||||
|  |         lon: 151.278_490_730_270_48, | ||||||
|  |         results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'Ravenhall', | ||||||
|  |         lat: -37.765_732_399_174_75, | ||||||
|  |         lon: 144.752_453_164_883_3, | ||||||
|  |         results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'Scarborough', | ||||||
|  |         lat: -31.894_346_156_789_997, | ||||||
|  |         lon: 115.757_617_103_904_64, | ||||||
|  |         results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }], | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     it.each(reverseGeocodeTestCases)(`should resolve to $name`, async ({ lat, lon, results }) => { | ||||||
|  |       const { status, body } = await request(app) | ||||||
|  |         .get(`/map/reverse-geocode?lat=${lat}&lon=${lon}`) | ||||||
|  |         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||||
|  |       expect(status).toBe(200); | ||||||
|  |       expect(Array.isArray(body)).toBe(true); | ||||||
|  |       expect(body.length).toBe(results.length); | ||||||
|  |       expect(body).toEqual(results); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @ -146,6 +146,7 @@ Class | Method | HTTP request | Description | |||||||
| *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |  | *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |  | ||||||
| *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |  | *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |  | ||||||
| *MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |  | *MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |  | ||||||
|  | *MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode |  | ||||||
| *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |  | *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |  | ||||||
| *MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |  | *MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |  | ||||||
| *MemoriesApi* | [**deleteMemory**](doc//MemoriesApi.md#deletememory) | **DELETE** /memories/{id} |  | *MemoriesApi* | [**deleteMemory**](doc//MemoriesApi.md#deletememory) | **DELETE** /memories/{id} |  | ||||||
| @ -339,6 +340,7 @@ Class | Method | HTTP request | Description | |||||||
|  - [LoginResponseDto](doc//LoginResponseDto.md) |  - [LoginResponseDto](doc//LoginResponseDto.md) | ||||||
|  - [LogoutResponseDto](doc//LogoutResponseDto.md) |  - [LogoutResponseDto](doc//LogoutResponseDto.md) | ||||||
|  - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) |  - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) | ||||||
|  |  - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) | ||||||
|  - [MapTheme](doc//MapTheme.md) |  - [MapTheme](doc//MapTheme.md) | ||||||
|  - [MemoryCreateDto](doc//MemoryCreateDto.md) |  - [MemoryCreateDto](doc//MemoryCreateDto.md) | ||||||
|  - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) |  - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @ -152,6 +152,7 @@ part 'model/login_credential_dto.dart'; | |||||||
| part 'model/login_response_dto.dart'; | part 'model/login_response_dto.dart'; | ||||||
| part 'model/logout_response_dto.dart'; | part 'model/logout_response_dto.dart'; | ||||||
| part 'model/map_marker_response_dto.dart'; | part 'model/map_marker_response_dto.dart'; | ||||||
|  | part 'model/map_reverse_geocode_response_dto.dart'; | ||||||
| part 'model/map_theme.dart'; | part 'model/map_theme.dart'; | ||||||
| part 'model/memory_create_dto.dart'; | part 'model/memory_create_dto.dart'; | ||||||
| part 'model/memory_lane_response_dto.dart'; | part 'model/memory_lane_response_dto.dart'; | ||||||
|  | |||||||
							
								
								
									
										57
									
								
								mobile/openapi/lib/api/map_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										57
									
								
								mobile/openapi/lib/api/map_api.dart
									
									
									
										generated
									
									
									
								
							| @ -160,4 +160,61 @@ class MapApi { | |||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response]. | ||||||
|  |   /// Parameters: | ||||||
|  |   /// | ||||||
|  |   /// * [double] lat (required): | ||||||
|  |   /// | ||||||
|  |   /// * [double] lon (required): | ||||||
|  |   Future<Response> reverseGeocodeWithHttpInfo(double lat, double lon,) async { | ||||||
|  |     // ignore: prefer_const_declarations | ||||||
|  |     final path = r'/map/reverse-geocode'; | ||||||
|  | 
 | ||||||
|  |     // ignore: prefer_final_locals | ||||||
|  |     Object? postBody; | ||||||
|  | 
 | ||||||
|  |     final queryParams = <QueryParam>[]; | ||||||
|  |     final headerParams = <String, String>{}; | ||||||
|  |     final formParams = <String, String>{}; | ||||||
|  | 
 | ||||||
|  |       queryParams.addAll(_queryParams('', 'lat', lat)); | ||||||
|  |       queryParams.addAll(_queryParams('', 'lon', lon)); | ||||||
|  | 
 | ||||||
|  |     const contentTypes = <String>[]; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     return apiClient.invokeAPI( | ||||||
|  |       path, | ||||||
|  |       'GET', | ||||||
|  |       queryParams, | ||||||
|  |       postBody, | ||||||
|  |       headerParams, | ||||||
|  |       formParams, | ||||||
|  |       contentTypes.isEmpty ? null : contentTypes.first, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Parameters: | ||||||
|  |   /// | ||||||
|  |   /// * [double] lat (required): | ||||||
|  |   /// | ||||||
|  |   /// * [double] lon (required): | ||||||
|  |   Future<List<MapReverseGeocodeResponseDto>?> reverseGeocode(double lat, double lon,) async { | ||||||
|  |     final response = await reverseGeocodeWithHttpInfo(lat, lon,); | ||||||
|  |     if (response.statusCode >= HttpStatus.badRequest) { | ||||||
|  |       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||||
|  |     } | ||||||
|  |     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||||
|  |     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||||
|  |     // FormatException when trying to decode an empty string. | ||||||
|  |     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||||
|  |       final responseBody = await _decodeBodyBytes(response); | ||||||
|  |       return (await apiClient.deserializeAsync(responseBody, 'List<MapReverseGeocodeResponseDto>') as List) | ||||||
|  |         .cast<MapReverseGeocodeResponseDto>() | ||||||
|  |         .toList(growable: false); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @ -362,6 +362,8 @@ class ApiClient { | |||||||
|           return LogoutResponseDto.fromJson(value); |           return LogoutResponseDto.fromJson(value); | ||||||
|         case 'MapMarkerResponseDto': |         case 'MapMarkerResponseDto': | ||||||
|           return MapMarkerResponseDto.fromJson(value); |           return MapMarkerResponseDto.fromJson(value); | ||||||
|  |         case 'MapReverseGeocodeResponseDto': | ||||||
|  |           return MapReverseGeocodeResponseDto.fromJson(value); | ||||||
|         case 'MapTheme': |         case 'MapTheme': | ||||||
|           return MapThemeTypeTransformer().decode(value); |           return MapThemeTypeTransformer().decode(value); | ||||||
|         case 'MemoryCreateDto': |         case 'MemoryCreateDto': | ||||||
|  | |||||||
							
								
								
									
										126
									
								
								mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | // | ||||||
|  | // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||||
|  | // | ||||||
|  | // @dart=2.18 | ||||||
|  | 
 | ||||||
|  | // ignore_for_file: unused_element, unused_import | ||||||
|  | // ignore_for_file: always_put_required_named_parameters_first | ||||||
|  | // ignore_for_file: constant_identifier_names | ||||||
|  | // ignore_for_file: lines_longer_than_80_chars | ||||||
|  | 
 | ||||||
|  | part of openapi.api; | ||||||
|  | 
 | ||||||
|  | class MapReverseGeocodeResponseDto { | ||||||
|  |   /// Returns a new [MapReverseGeocodeResponseDto] instance. | ||||||
|  |   MapReverseGeocodeResponseDto({ | ||||||
|  |     required this.city, | ||||||
|  |     required this.country, | ||||||
|  |     required this.state, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   String? city; | ||||||
|  | 
 | ||||||
|  |   String? country; | ||||||
|  | 
 | ||||||
|  |   String? state; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) => identical(this, other) || other is MapReverseGeocodeResponseDto && | ||||||
|  |     other.city == city && | ||||||
|  |     other.country == country && | ||||||
|  |     other.state == state; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int get hashCode => | ||||||
|  |     // ignore: unnecessary_parenthesis | ||||||
|  |     (city == null ? 0 : city!.hashCode) + | ||||||
|  |     (country == null ? 0 : country!.hashCode) + | ||||||
|  |     (state == null ? 0 : state!.hashCode); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   String toString() => 'MapReverseGeocodeResponseDto[city=$city, country=$country, state=$state]'; | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |     final json = <String, dynamic>{}; | ||||||
|  |     if (this.city != null) { | ||||||
|  |       json[r'city'] = this.city; | ||||||
|  |     } else { | ||||||
|  |     //  json[r'city'] = null; | ||||||
|  |     } | ||||||
|  |     if (this.country != null) { | ||||||
|  |       json[r'country'] = this.country; | ||||||
|  |     } else { | ||||||
|  |     //  json[r'country'] = null; | ||||||
|  |     } | ||||||
|  |     if (this.state != null) { | ||||||
|  |       json[r'state'] = this.state; | ||||||
|  |     } else { | ||||||
|  |     //  json[r'state'] = null; | ||||||
|  |     } | ||||||
|  |     return json; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Returns a new [MapReverseGeocodeResponseDto] instance and imports its values from | ||||||
|  |   /// [value] if it's a [Map], null otherwise. | ||||||
|  |   // ignore: prefer_constructors_over_static_methods | ||||||
|  |   static MapReverseGeocodeResponseDto? fromJson(dynamic value) { | ||||||
|  |     if (value is Map) { | ||||||
|  |       final json = value.cast<String, dynamic>(); | ||||||
|  | 
 | ||||||
|  |       return MapReverseGeocodeResponseDto( | ||||||
|  |         city: mapValueOfType<String>(json, r'city'), | ||||||
|  |         country: mapValueOfType<String>(json, r'country'), | ||||||
|  |         state: mapValueOfType<String>(json, r'state'), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static List<MapReverseGeocodeResponseDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final result = <MapReverseGeocodeResponseDto>[]; | ||||||
|  |     if (json is List && json.isNotEmpty) { | ||||||
|  |       for (final row in json) { | ||||||
|  |         final value = MapReverseGeocodeResponseDto.fromJson(row); | ||||||
|  |         if (value != null) { | ||||||
|  |           result.add(value); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return result.toList(growable: growable); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static Map<String, MapReverseGeocodeResponseDto> mapFromJson(dynamic json) { | ||||||
|  |     final map = <String, MapReverseGeocodeResponseDto>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         final value = MapReverseGeocodeResponseDto.fromJson(entry.value); | ||||||
|  |         if (value != null) { | ||||||
|  |           map[entry.key] = value; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // maps a json object with a list of MapReverseGeocodeResponseDto-objects as value to a dart map | ||||||
|  |   static Map<String, List<MapReverseGeocodeResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final map = <String, List<MapReverseGeocodeResponseDto>>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       // ignore: parameter_assignments | ||||||
|  |       json = json.cast<String, dynamic>(); | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         map[entry.key] = MapReverseGeocodeResponseDto.listFromJson(entry.value, growable: growable,); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// The list of required keys that must be present in a JSON. | ||||||
|  |   static const requiredKeys = <String>{ | ||||||
|  |     'city', | ||||||
|  |     'country', | ||||||
|  |     'state', | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -3109,6 +3109,60 @@ | |||||||
|         ] |         ] | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/map/reverse-geocode": { | ||||||
|  |       "get": { | ||||||
|  |         "operationId": "reverseGeocode", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "name": "lat", | ||||||
|  |             "required": true, | ||||||
|  |             "in": "query", | ||||||
|  |             "schema": { | ||||||
|  |               "format": "double", | ||||||
|  |               "type": "number" | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "name": "lon", | ||||||
|  |             "required": true, | ||||||
|  |             "in": "query", | ||||||
|  |             "schema": { | ||||||
|  |               "format": "double", | ||||||
|  |               "type": "number" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "content": { | ||||||
|  |               "application/json": { | ||||||
|  |                 "schema": { | ||||||
|  |                   "items": { | ||||||
|  |                     "$ref": "#/components/schemas/MapReverseGeocodeResponseDto" | ||||||
|  |                   }, | ||||||
|  |                   "type": "array" | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             "description": "" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "security": [ | ||||||
|  |           { | ||||||
|  |             "bearer": [] | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "cookie": [] | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "api_key": [] | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "Map" | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/map/style.json": { |     "/map/style.json": { | ||||||
|       "get": { |       "get": { | ||||||
|         "operationId": "getMapStyle", |         "operationId": "getMapStyle", | ||||||
| @ -9128,6 +9182,28 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
|  |       "MapReverseGeocodeResponseDto": { | ||||||
|  |         "properties": { | ||||||
|  |           "city": { | ||||||
|  |             "nullable": true, | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "country": { | ||||||
|  |             "nullable": true, | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "state": { | ||||||
|  |             "nullable": true, | ||||||
|  |             "type": "string" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "required": [ | ||||||
|  |           "city", | ||||||
|  |           "country", | ||||||
|  |           "state" | ||||||
|  |         ], | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "MapTheme": { |       "MapTheme": { | ||||||
|         "enum": [ |         "enum": [ | ||||||
|           "light", |           "light", | ||||||
|  | |||||||
| @ -554,6 +554,11 @@ export type MapMarkerResponseDto = { | |||||||
|     lon: number; |     lon: number; | ||||||
|     state: string | null; |     state: string | null; | ||||||
| }; | }; | ||||||
|  | export type MapReverseGeocodeResponseDto = { | ||||||
|  |     city: string | null; | ||||||
|  |     country: string | null; | ||||||
|  |     state: string | null; | ||||||
|  | }; | ||||||
| export type OnThisDayDto = { | export type OnThisDayDto = { | ||||||
|     year: number; |     year: number; | ||||||
| }; | }; | ||||||
| @ -1991,6 +1996,20 @@ export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, | |||||||
|         ...opts |         ...opts | ||||||
|     })); |     })); | ||||||
| } | } | ||||||
|  | export function reverseGeocode({ lat, lon }: { | ||||||
|  |     lat: number; | ||||||
|  |     lon: number; | ||||||
|  | }, opts?: Oazapfts.RequestOpts) { | ||||||
|  |     return oazapfts.ok(oazapfts.fetchJson<{ | ||||||
|  |         status: 200; | ||||||
|  |         data: MapReverseGeocodeResponseDto[]; | ||||||
|  |     }>(`/map/reverse-geocode${QS.query(QS.explode({ | ||||||
|  |         lat, | ||||||
|  |         lon | ||||||
|  |     }))}`, {
 | ||||||
|  |         ...opts | ||||||
|  |     })); | ||||||
|  | } | ||||||
| export function getMapStyle({ key, theme }: { | export function getMapStyle({ key, theme }: { | ||||||
|     key?: string; |     key?: string; | ||||||
|     theme: MapTheme; |     theme: MapTheme; | ||||||
|  | |||||||
| @ -1,7 +1,12 @@ | |||||||
| import { Controller, Get, Query } from '@nestjs/common'; | import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common'; | ||||||
| import { ApiTags } from '@nestjs/swagger'; | import { ApiTags } from '@nestjs/swagger'; | ||||||
| import { AuthDto } from 'src/dtos/auth.dto'; | import { AuthDto } from 'src/dtos/auth.dto'; | ||||||
| import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto'; | import { | ||||||
|  |   MapMarkerDto, | ||||||
|  |   MapMarkerResponseDto, | ||||||
|  |   MapReverseGeocodeDto, | ||||||
|  |   MapReverseGeocodeResponseDto, | ||||||
|  | } from 'src/dtos/map.dto'; | ||||||
| import { MapThemeDto } from 'src/dtos/system-config.dto'; | import { MapThemeDto } from 'src/dtos/system-config.dto'; | ||||||
| import { Auth, Authenticated } from 'src/middleware/auth.guard'; | import { Auth, Authenticated } from 'src/middleware/auth.guard'; | ||||||
| import { MapService } from 'src/services/map.service'; | import { MapService } from 'src/services/map.service'; | ||||||
| @ -22,4 +27,11 @@ export class MapController { | |||||||
|   getMapStyle(@Query() dto: MapThemeDto) { |   getMapStyle(@Query() dto: MapThemeDto) { | ||||||
|     return this.service.getMapStyle(dto.theme); |     return this.service.getMapStyle(dto.theme); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   @Authenticated() | ||||||
|  |   @Get('reverse-geocode') | ||||||
|  |   @HttpCode(HttpStatus.OK) | ||||||
|  |   reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise<MapReverseGeocodeResponseDto[]> { | ||||||
|  |     return this.service.reverseGeocode(dto); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										67
									
								
								server/src/dtos/map.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								server/src/dtos/map.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | |||||||
|  | import { ApiProperty } from '@nestjs/swagger'; | ||||||
|  | import { Type } from 'class-transformer'; | ||||||
|  | import { IsLatitude, IsLongitude } from 'class-validator'; | ||||||
|  | import { ValidateBoolean, ValidateDate } from 'src/validation'; | ||||||
|  | 
 | ||||||
|  | export class MapReverseGeocodeDto { | ||||||
|  |   @ApiProperty({ format: 'double' }) | ||||||
|  |   @Type(() => Number) | ||||||
|  |   @IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` }) | ||||||
|  |   lat!: number; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty({ format: 'double' }) | ||||||
|  |   @Type(() => Number) | ||||||
|  |   @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) | ||||||
|  |   lon!: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class MapReverseGeocodeResponseDto { | ||||||
|  |   @ApiProperty() | ||||||
|  |   city!: string | null; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty() | ||||||
|  |   state!: string | null; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty() | ||||||
|  |   country!: string | null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class MapMarkerDto { | ||||||
|  |   @ValidateBoolean({ optional: true }) | ||||||
|  |   isArchived?: boolean; | ||||||
|  | 
 | ||||||
|  |   @ValidateBoolean({ optional: true }) | ||||||
|  |   isFavorite?: boolean; | ||||||
|  | 
 | ||||||
|  |   @ValidateDate({ optional: true }) | ||||||
|  |   fileCreatedAfter?: Date; | ||||||
|  | 
 | ||||||
|  |   @ValidateDate({ optional: true }) | ||||||
|  |   fileCreatedBefore?: Date; | ||||||
|  | 
 | ||||||
|  |   @ValidateBoolean({ optional: true }) | ||||||
|  |   withPartners?: boolean; | ||||||
|  | 
 | ||||||
|  |   @ValidateBoolean({ optional: true }) | ||||||
|  |   withSharedAlbums?: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class MapMarkerResponseDto { | ||||||
|  |   @ApiProperty() | ||||||
|  |   id!: string; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty({ format: 'double' }) | ||||||
|  |   lat!: number; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty({ format: 'double' }) | ||||||
|  |   lon!: number; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty() | ||||||
|  |   city!: string | null; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty() | ||||||
|  |   state!: string | null; | ||||||
|  | 
 | ||||||
|  |   @ApiProperty() | ||||||
|  |   country!: string | null; | ||||||
|  | } | ||||||
| @ -289,26 +289,6 @@ export class SearchExploreResponseDto { | |||||||
|   items!: SearchExploreItem[]; |   items!: SearchExploreItem[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class MapMarkerDto { |  | ||||||
|   @ValidateBoolean({ optional: true }) |  | ||||||
|   isArchived?: boolean; |  | ||||||
| 
 |  | ||||||
|   @ValidateBoolean({ optional: true }) |  | ||||||
|   isFavorite?: boolean; |  | ||||||
| 
 |  | ||||||
|   @ValidateDate({ optional: true }) |  | ||||||
|   fileCreatedAfter?: Date; |  | ||||||
| 
 |  | ||||||
|   @ValidateDate({ optional: true }) |  | ||||||
|   fileCreatedBefore?: Date; |  | ||||||
| 
 |  | ||||||
|   @ValidateBoolean({ optional: true }) |  | ||||||
|   withPartners?: boolean; |  | ||||||
| 
 |  | ||||||
|   @ValidateBoolean({ optional: true }) |  | ||||||
|   withSharedAlbums?: boolean; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class MemoryLaneDto { | export class MemoryLaneDto { | ||||||
|   @IsInt() |   @IsInt() | ||||||
|   @Type(() => Number) |   @Type(() => Number) | ||||||
| @ -324,22 +304,3 @@ export class MemoryLaneDto { | |||||||
|   @ApiProperty({ type: 'integer' }) |   @ApiProperty({ type: 'integer' }) | ||||||
|   month!: number; |   month!: number; | ||||||
| } | } | ||||||
| export class MapMarkerResponseDto { |  | ||||||
|   @ApiProperty() |  | ||||||
|   id!: string; |  | ||||||
| 
 |  | ||||||
|   @ApiProperty({ format: 'double' }) |  | ||||||
|   lat!: number; |  | ||||||
| 
 |  | ||||||
|   @ApiProperty({ format: 'double' }) |  | ||||||
|   lon!: number; |  | ||||||
| 
 |  | ||||||
|   @ApiProperty() |  | ||||||
|   city!: string | null; |  | ||||||
| 
 |  | ||||||
|   @ApiProperty() |  | ||||||
|   state!: string | null; |  | ||||||
| 
 |  | ||||||
|   @ApiProperty() |  | ||||||
|   country!: string | null; |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { Inject } from '@nestjs/common'; | import { Inject } from '@nestjs/common'; | ||||||
| import { SystemConfigCore } from 'src/cores/system-config.core'; | import { SystemConfigCore } from 'src/cores/system-config.core'; | ||||||
| import { AuthDto } from 'src/dtos/auth.dto'; | import { AuthDto } from 'src/dtos/auth.dto'; | ||||||
| import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto'; | import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto'; | ||||||
| import { IAlbumRepository } from 'src/interfaces/album.interface'; | import { IAlbumRepository } from 'src/interfaces/album.interface'; | ||||||
| import { ILoggerRepository } from 'src/interfaces/logger.interface'; | import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
| import { IMapRepository } from 'src/interfaces/map.interface'; | import { IMapRepository } from 'src/interfaces/map.interface'; | ||||||
| @ -53,4 +53,11 @@ export class MapService { | |||||||
| 
 | 
 | ||||||
|     return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`)); |     return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`)); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   async reverseGeocode(dto: MapReverseGeocodeDto) { | ||||||
|  |     const { lat: latitude, lon: longitude } = dto; | ||||||
|  |     // eventually this should probably return an array of results
 | ||||||
|  |     const result = await this.mapRepository.reverseGeocode({ latitude, longitude }); | ||||||
|  |     return result ? [result] : []; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user