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' })); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   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 |  | ||||
| *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |  | ||||
| *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* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |  | ||||
| *MemoriesApi* | [**deleteMemory**](doc//MemoriesApi.md#deletememory) | **DELETE** /memories/{id} |  | ||||
| @ -339,6 +340,7 @@ Class | Method | HTTP request | Description | ||||
|  - [LoginResponseDto](doc//LoginResponseDto.md) | ||||
|  - [LogoutResponseDto](doc//LogoutResponseDto.md) | ||||
|  - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) | ||||
|  - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) | ||||
|  - [MapTheme](doc//MapTheme.md) | ||||
|  - [MemoryCreateDto](doc//MemoryCreateDto.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/logout_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/memory_create_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; | ||||
|   } | ||||
| 
 | ||||
|   /// 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); | ||||
|         case 'MapMarkerResponseDto': | ||||
|           return MapMarkerResponseDto.fromJson(value); | ||||
|         case 'MapReverseGeocodeResponseDto': | ||||
|           return MapReverseGeocodeResponseDto.fromJson(value); | ||||
|         case 'MapTheme': | ||||
|           return MapThemeTypeTransformer().decode(value); | ||||
|         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": { | ||||
|       "get": { | ||||
|         "operationId": "getMapStyle", | ||||
| @ -9128,6 +9182,28 @@ | ||||
|         ], | ||||
|         "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": { | ||||
|         "enum": [ | ||||
|           "light", | ||||
|  | ||||
| @ -554,6 +554,11 @@ export type MapMarkerResponseDto = { | ||||
|     lon: number; | ||||
|     state: string | null; | ||||
| }; | ||||
| export type MapReverseGeocodeResponseDto = { | ||||
|     city: string | null; | ||||
|     country: string | null; | ||||
|     state: string | null; | ||||
| }; | ||||
| export type OnThisDayDto = { | ||||
|     year: number; | ||||
| }; | ||||
| @ -1991,6 +1996,20 @@ export function getMapMarkers({ fileCreatedAfter, fileCreatedBefore, isArchived, | ||||
|         ...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 }: { | ||||
|     key?: string; | ||||
|     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 { 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 { Auth, Authenticated } from 'src/middleware/auth.guard'; | ||||
| import { MapService } from 'src/services/map.service'; | ||||
| @ -22,4 +27,11 @@ export class MapController { | ||||
|   getMapStyle(@Query() dto: MapThemeDto) { | ||||
|     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[]; | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
|   @IsInt() | ||||
|   @Type(() => Number) | ||||
| @ -324,22 +304,3 @@ export class MemoryLaneDto { | ||||
|   @ApiProperty({ type: 'integer' }) | ||||
|   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 { SystemConfigCore } from 'src/cores/system-config.core'; | ||||
| 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 { ILoggerRepository } from 'src/interfaces/logger.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`)); | ||||
|   } | ||||
| 
 | ||||
|   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