mirror of
https://github.com/immich-app/immich.git
synced 2025-01-21 00:52:43 -05: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
12 changed files with 443 additions and 42 deletions
|
@ -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…
Add table
Reference in a new issue