Browse Source

Add API endpoint that returns only assets with location information (Thanks @EPP100)

Matthias Rupp 2 years ago
parent
commit
1b1d2f0ba4

+ 3 - 0
mobile/openapi/.openapi-generator/FILES

@@ -55,6 +55,7 @@ doc/JobStatusDto.md
 doc/LoginCredentialDto.md
 doc/LoginResponseDto.md
 doc/LogoutResponseDto.md
+doc/MapMarkerResponseDto.md
 doc/OAuthApi.md
 doc/OAuthCallbackDto.md
 doc/OAuthConfigDto.md
@@ -171,6 +172,7 @@ lib/model/job_status_dto.dart
 lib/model/login_credential_dto.dart
 lib/model/login_response_dto.dart
 lib/model/logout_response_dto.dart
+lib/model/map_marker_response_dto.dart
 lib/model/o_auth_callback_dto.dart
 lib/model/o_auth_config_dto.dart
 lib/model/o_auth_config_response_dto.dart
@@ -264,6 +266,7 @@ test/job_status_dto_test.dart
 test/login_credential_dto_test.dart
 test/login_response_dto_test.dart
 test/logout_response_dto_test.dart
+test/map_marker_response_dto_test.dart
 test/o_auth_api_test.dart
 test/o_auth_callback_dto_test.dart
 test/o_auth_config_dto_test.dart

+ 2 - 0
mobile/openapi/README.md

@@ -103,6 +103,7 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} | 
 *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
+*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/mapMarker | 
 *AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 *AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove | 
 *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | 
@@ -205,6 +206,7 @@ Class | Method | HTTP request | Description
  - [LoginCredentialDto](doc//LoginCredentialDto.md)
  - [LoginResponseDto](doc//LoginResponseDto.md)
  - [LogoutResponseDto](doc//LogoutResponseDto.md)
+ - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
  - [OAuthCallbackDto](doc//OAuthCallbackDto.md)
  - [OAuthConfigDto](doc//OAuthConfigDto.md)
  - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)

+ 58 - 0
mobile/openapi/doc/AssetApi.md

@@ -27,6 +27,7 @@ Method | HTTP request | Description
 [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{assetId} | 
 [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
+[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/mapMarker | 
 [**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 [**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove | 
 [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | 
@@ -967,6 +968,63 @@ This endpoint does not need any parameter.
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
+# **getMapMarkers**
+> List<MapMarkerResponseDto> getMapMarkers(isFavorite, isArchived, skip)
+
+
+
+Get all assets that have GPS information embedded
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = AssetApi();
+final isFavorite = true; // bool | 
+final isArchived = true; // bool | 
+final skip = 8.14; // num | 
+
+try {
+    final result = api_instance.getMapMarkers(isFavorite, isArchived, skip);
+    print(result);
+} catch (e) {
+    print('Exception when calling AssetApi->getMapMarkers: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **isFavorite** | **bool**|  | [optional] 
+ **isArchived** | **bool**|  | [optional] 
+ **skip** | **num**|  | [optional] 
+
+### Return type
+
+[**List<MapMarkerResponseDto>**](MapMarkerResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
 # **getUserAssetsByDeviceId**
 > List<String> getUserAssetsByDeviceId(deviceId)
 

+ 18 - 0
mobile/openapi/doc/MapMarkerResponseDto.md

@@ -0,0 +1,18 @@
+# openapi.model.MapMarkerResponseDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **String** |  | 
+**type** | **String** |  | 
+**lat** | **num** |  | 
+**lon** | **num** |  | 
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+

+ 1 - 0
mobile/openapi/lib/api.dart

@@ -88,6 +88,7 @@ part 'model/job_status_dto.dart';
 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/o_auth_callback_dto.dart';
 part 'model/o_auth_config_dto.dart';
 part 'model/o_auth_config_response_dto.dart';

+ 73 - 0
mobile/openapi/lib/api/asset_api.dart

@@ -979,6 +979,79 @@ class AssetApi {
     return null;
   }
 
+  /// Get all assets that have GPS information embedded
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
+  /// Parameters:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [bool] isArchived:
+  ///
+  /// * [num] skip:
+  Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, bool? isArchived, num? skip, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/asset/mapMarker';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    if (isFavorite != null) {
+      queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
+    }
+    if (isArchived != null) {
+      queryParams.addAll(_queryParams('', 'isArchived', isArchived));
+    }
+    if (skip != null) {
+      queryParams.addAll(_queryParams('', 'skip', skip));
+    }
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Get all assets that have GPS information embedded
+  ///
+  /// Parameters:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [bool] isArchived:
+  ///
+  /// * [num] skip:
+  Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, bool? isArchived, num? skip, }) async {
+    final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, isArchived: isArchived, skip: skip, );
+    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<MapMarkerResponseDto>') as List)
+        .cast<MapMarkerResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
   /// Get all asset of a device that are in the database, ID only.
   ///
   /// Note: This method returns the HTTP [Response].

+ 2 - 0
mobile/openapi/lib/api_client.dart

@@ -275,6 +275,8 @@ class ApiClient {
           return LoginResponseDto.fromJson(value);
         case 'LogoutResponseDto':
           return LogoutResponseDto.fromJson(value);
+        case 'MapMarkerResponseDto':
+          return MapMarkerResponseDto.fromJson(value);
         case 'OAuthCallbackDto':
           return OAuthCallbackDto.fromJson(value);
         case 'OAuthConfigDto':

+ 219 - 0
mobile/openapi/lib/model/map_marker_response_dto.dart

@@ -0,0 +1,219 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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 MapMarkerResponseDto {
+  /// Returns a new [MapMarkerResponseDto] instance.
+  MapMarkerResponseDto({
+    required this.id,
+    required this.type,
+    required this.lat,
+    required this.lon,
+  });
+
+  String id;
+
+  MapMarkerResponseDtoTypeEnum type;
+
+  num lat;
+
+  num lon;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is MapMarkerResponseDto &&
+     other.id == id &&
+     other.type == type &&
+     other.lat == lat &&
+     other.lon == lon;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (id.hashCode) +
+    (type.hashCode) +
+    (lat.hashCode) +
+    (lon.hashCode);
+
+  @override
+  String toString() => 'MapMarkerResponseDto[id=$id, type=$type, lat=$lat, lon=$lon]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'id'] = this.id;
+      json[r'type'] = this.type;
+      json[r'lat'] = this.lat;
+      json[r'lon'] = this.lon;
+    return json;
+  }
+
+  /// Returns a new [MapMarkerResponseDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static MapMarkerResponseDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "MapMarkerResponseDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "MapMarkerResponseDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return MapMarkerResponseDto(
+        id: mapValueOfType<String>(json, r'id')!,
+        type: MapMarkerResponseDtoTypeEnum.fromJson(json[r'type'])!,
+        lat: json[r'lat'] == null
+            ? null
+            : num.parse(json[r'lat'].toString()),
+        lon: json[r'lon'] == null
+            ? null
+            : num.parse(json[r'lon'].toString()),
+      );
+    }
+    return null;
+  }
+
+  static List<MapMarkerResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <MapMarkerResponseDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = MapMarkerResponseDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, MapMarkerResponseDto> mapFromJson(dynamic json) {
+    final map = <String, MapMarkerResponseDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = MapMarkerResponseDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of MapMarkerResponseDto-objects as value to a dart map
+  static Map<String, List<MapMarkerResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<MapMarkerResponseDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = MapMarkerResponseDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'id',
+    'type',
+    'lat',
+    'lon',
+  };
+}
+
+
+class MapMarkerResponseDtoTypeEnum {
+  /// Instantiate a new enum with the provided [value].
+  const MapMarkerResponseDtoTypeEnum._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const IMAGE = MapMarkerResponseDtoTypeEnum._(r'IMAGE');
+  static const VIDEO = MapMarkerResponseDtoTypeEnum._(r'VIDEO');
+  static const AUDIO = MapMarkerResponseDtoTypeEnum._(r'AUDIO');
+  static const OTHER = MapMarkerResponseDtoTypeEnum._(r'OTHER');
+
+  /// List of all possible values in this [enum][MapMarkerResponseDtoTypeEnum].
+  static const values = <MapMarkerResponseDtoTypeEnum>[
+    IMAGE,
+    VIDEO,
+    AUDIO,
+    OTHER,
+  ];
+
+  static MapMarkerResponseDtoTypeEnum? fromJson(dynamic value) => MapMarkerResponseDtoTypeEnumTypeTransformer().decode(value);
+
+  static List<MapMarkerResponseDtoTypeEnum>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <MapMarkerResponseDtoTypeEnum>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = MapMarkerResponseDtoTypeEnum.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [MapMarkerResponseDtoTypeEnum] to String,
+/// and [decode] dynamic data back to [MapMarkerResponseDtoTypeEnum].
+class MapMarkerResponseDtoTypeEnumTypeTransformer {
+  factory MapMarkerResponseDtoTypeEnumTypeTransformer() => _instance ??= const MapMarkerResponseDtoTypeEnumTypeTransformer._();
+
+  const MapMarkerResponseDtoTypeEnumTypeTransformer._();
+
+  String encode(MapMarkerResponseDtoTypeEnum data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a MapMarkerResponseDtoTypeEnum.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  MapMarkerResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'IMAGE': return MapMarkerResponseDtoTypeEnum.IMAGE;
+        case r'VIDEO': return MapMarkerResponseDtoTypeEnum.VIDEO;
+        case r'AUDIO': return MapMarkerResponseDtoTypeEnum.AUDIO;
+        case r'OTHER': return MapMarkerResponseDtoTypeEnum.OTHER;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [MapMarkerResponseDtoTypeEnumTypeTransformer] instance.
+  static MapMarkerResponseDtoTypeEnumTypeTransformer? _instance;
+}
+
+

+ 7 - 0
mobile/openapi/test/asset_api_test.dart

@@ -117,6 +117,13 @@ void main() {
       // TODO
     });
 
+    // Get all assets that have GPS information embedded
+    //
+    //Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, bool isArchived, num skip }) async
+    test('test getMapMarkers', () async {
+      // TODO
+    });
+
     // Get all asset of a device that are in the database, ID only.
     //
     //Future<List<String>> getUserAssetsByDeviceId(String deviceId) async

+ 42 - 0
mobile/openapi/test/map_marker_response_dto_test.dart

@@ -0,0 +1,42 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for MapMarkerResponseDto
+void main() {
+  // final instance = MapMarkerResponseDto();
+
+  group('test MapMarkerResponseDto', () {
+    // String id
+    test('to test the property `id`', () async {
+      // TODO
+    });
+
+    // String type
+    test('to test the property `type`', () async {
+      // TODO
+    });
+
+    // num lat
+    test('to test the property `lat`', () async {
+      // TODO
+    });
+
+    // num lon
+    test('to test the property `lon`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 13 - 1
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -31,7 +31,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
-import { AssetResponseDto, ImmichReadStream } from '@app/domain';
+import { AssetResponseDto, ImmichReadStream, MapMarkerResponseDto } from '@app/domain';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
 import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
@@ -260,6 +260,18 @@ export class AssetController {
     return await this.assetService.getAssetByTimeBucket(authUser, getAssetByTimeBucketDto);
   }
 
+  /**
+   * Get all assets that have GPS information embedded
+   */
+  @Authenticated()
+  @Get('/mapMarker')
+  getMapMarkers(
+    @GetAuthUser() authUser: AuthUserDto,
+    @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
+  ): Promise<MapMarkerResponseDto[]> {
+    return this.assetService.getMapMarkers(authUser, dto);
+  }
+
   /**
    * Get all asset of a device that are in the database, ID only.
    */

+ 10 - 0
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -30,6 +30,8 @@ import {
   JobName,
   mapAsset,
   mapAssetWithoutExif,
+  MapMarkerResponseDto,
+  mapAssetMapMarker,
 } from '@app/domain';
 import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
 import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
@@ -142,6 +144,14 @@ export class AssetService {
     return assets.map((asset) => mapAsset(asset));
   }
 
+  public async getMapMarkers(authUser: AuthUserDto, dto: AssetSearchDto): Promise<MapMarkerResponseDto[]> {
+    const assets = await this._assetRepository.getAllByUserId(authUser.id, dto);
+
+    return assets
+      .map((asset) => mapAssetMapMarker(asset))
+      .filter((marker) => marker != null) as MapMarkerResponseDto[];
+  }
+
   public async getAssetByTimeBucket(
     authUser: AuthUserDto,
     getAssetByTimeBucketDto: GetAssetByTimeBucketDto,

+ 87 - 0
server/immich-openapi-specs.json

@@ -2436,6 +2436,64 @@
         ]
       }
     },
+    "/asset/mapMarker": {
+      "get": {
+        "operationId": "getMapMarkers",
+        "description": "Get all assets that have GPS information embedded",
+        "parameters": [
+          {
+            "name": "isFavorite",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "isArchived",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "skip",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "number"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/MapMarkerResponseDto"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Asset"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          }
+        ]
+      }
+    },
     "/asset/{deviceId}": {
       "get": {
         "operationId": "getUserAssetsByDeviceId",
@@ -5187,6 +5245,35 @@
           "timeBucket"
         ]
       },
+      "MapMarkerResponseDto": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string"
+          },
+          "type": {
+            "type": "string",
+            "enum": [
+              "IMAGE",
+              "VIDEO",
+              "AUDIO",
+              "OTHER"
+            ]
+          },
+          "lat": {
+            "type": "number"
+          },
+          "lon": {
+            "type": "number"
+          }
+        },
+        "required": [
+          "id",
+          "type",
+          "lat",
+          "lon"
+        ]
+      },
       "UpdateAssetDto": {
         "type": "object",
         "properties": {

+ 2 - 0
server/libs/domain/src/asset/response-dto/index.ts

@@ -1,3 +1,5 @@
 export * from './asset-response.dto';
 export * from './exif-response.dto';
 export * from './smart-info-response.dto';
+export * from './map-marker-response.dto';
+

+ 28 - 0
server/libs/domain/src/asset/response-dto/map-marker-response.dto.ts

@@ -0,0 +1,28 @@
+import { AssetEntity, AssetType } from '@app/infra/entities';
+
+export class MapMarkerResponseDto {
+  id!: string;
+  type!: AssetType;
+  lat!: number;
+  lon!: number;
+}
+
+export function mapAssetMapMarker(entity: AssetEntity): MapMarkerResponseDto | null {
+  if (!entity.exifInfo) {
+    return null;
+  }
+
+  const lat = entity.exifInfo.latitude;
+  const lon = entity.exifInfo.longitude;
+
+  if (!lat || !lon) {
+    return null;
+  }
+
+  return {
+    id: entity.id,
+    type: entity.type,
+    lon,
+    lat,
+  };
+}

+ 11 - 0
web/package-lock.json

@@ -14,6 +14,7 @@
 				"justified-layout": "^4.1.0",
 				"leaflet": "^1.9.3",
 				"leaflet.markercluster": "^1.5.3",
+				"leaflet.tilelayer.colorfilter": "^1.2.5",
 				"lodash-es": "^4.17.21",
 				"luxon": "^3.2.1",
 				"rxjs": "^7.8.0",
@@ -9053,6 +9054,11 @@
 				"leaflet": "^1.3.1"
 			}
 		},
+		"node_modules/leaflet.tilelayer.colorfilter": {
+			"version": "1.2.5",
+			"resolved": "https://registry.npmjs.org/leaflet.tilelayer.colorfilter/-/leaflet.tilelayer.colorfilter-1.2.5.tgz",
+			"integrity": "sha512-wUvqVlpEofDEDi0ocXApXAcz1l06RsNBEw3L/2ColaygDPERswgas2Jgv/DVrWrVd0HQAxDerwFqOtBI+zbw3w=="
+		},
 		"node_modules/leven": {
 			"version": "3.1.0",
 			"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -18060,6 +18066,11 @@
 			"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
 			"requires": {}
 		},
+		"leaflet.tilelayer.colorfilter": {
+			"version": "1.2.5",
+			"resolved": "https://registry.npmjs.org/leaflet.tilelayer.colorfilter/-/leaflet.tilelayer.colorfilter-1.2.5.tgz",
+			"integrity": "sha512-wUvqVlpEofDEDi0ocXApXAcz1l06RsNBEw3L/2ColaygDPERswgas2Jgv/DVrWrVd0HQAxDerwFqOtBI+zbw3w=="
+		},
 		"leven": {
 			"version": "3.1.0",
 			"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",

+ 1 - 0
web/package.json

@@ -62,6 +62,7 @@
 		"justified-layout": "^4.1.0",
 		"leaflet": "^1.9.3",
 		"leaflet.markercluster": "^1.5.3",
+		"leaflet.tilelayer.colorfilter": "^1.2.5",
 		"lodash-es": "^4.17.21",
 		"luxon": "^3.2.1",
 		"rxjs": "^7.8.0",

+ 127 - 0
web/src/api/open-api/api.ts

@@ -1438,6 +1438,47 @@ export interface LogoutResponseDto {
      */
     'redirectUri': string;
 }
+/**
+ * 
+ * @export
+ * @interface MapMarkerResponseDto
+ */
+export interface MapMarkerResponseDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof MapMarkerResponseDto
+     */
+    'id': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof MapMarkerResponseDto
+     */
+    'type': MapMarkerResponseDtoTypeEnum;
+    /**
+     * 
+     * @type {number}
+     * @memberof MapMarkerResponseDto
+     */
+    'lat': number;
+    /**
+     * 
+     * @type {number}
+     * @memberof MapMarkerResponseDto
+     */
+    'lon': number;
+}
+
+export const MapMarkerResponseDtoTypeEnum = {
+    Image: 'IMAGE',
+    Video: 'VIDEO',
+    Audio: 'AUDIO',
+    Other: 'OTHER'
+} as const;
+
+export type MapMarkerResponseDtoTypeEnum = typeof MapMarkerResponseDtoTypeEnum[keyof typeof MapMarkerResponseDtoTypeEnum];
+
 /**
  * 
  * @export
@@ -4647,6 +4688,56 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * Get all assets that have GPS information embedded
+         * @param {boolean} [isFavorite] 
+         * @param {boolean} [isArchived] 
+         * @param {number} [skip] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getMapMarkers: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/asset/mapMarker`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
+
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
+
+            if (skip !== undefined) {
+                localVarQueryParameter['skip'] = skip;
+            }
+
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -5198,6 +5289,18 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getCuratedObjects(options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * Get all assets that have GPS information embedded
+         * @param {boolean} [isFavorite] 
+         * @param {boolean} [isArchived] 
+         * @param {number} [skip] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, isArchived, skip, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * Get all asset of a device that are in the database, ID only.
          * @param {string} deviceId 
@@ -5454,6 +5557,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getCuratedObjects(options?: any): AxiosPromise<Array<CuratedObjectsResponseDto>> {
             return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath));
         },
+        /**
+         * Get all assets that have GPS information embedded
+         * @param {boolean} [isFavorite] 
+         * @param {boolean} [isArchived] 
+         * @param {number} [skip] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: any): AxiosPromise<Array<MapMarkerResponseDto>> {
+            return localVarFp.getMapMarkers(isFavorite, isArchived, skip, options).then((request) => request(axios, basePath));
+        },
         /**
          * Get all asset of a device that are in the database, ID only.
          * @param {string} deviceId 
@@ -5740,6 +5854,19 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getCuratedObjects(options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * Get all assets that have GPS information embedded
+     * @param {boolean} [isFavorite] 
+     * @param {boolean} [isArchived] 
+     * @param {number} [skip] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).getMapMarkers(isFavorite, isArchived, skip, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * Get all asset of a device that are in the database, ID only.
      * @param {string} deviceId 

+ 9 - 15
web/src/lib/components/shared-components/leaflet/asset-marker-cluster.svelte

@@ -12,11 +12,11 @@
 	import { onDestroy, onMount } from 'svelte';
 	import 'leaflet.markercluster';
 	import { getMapContext } from './map.svelte';
-	import { AssetResponseDto, getFileUrl } from '@api';
+	import { MapMarkerResponseDto, getFileUrl } from '@api';
 	import { Marker, Icon } from 'leaflet';
 	import { assetInteractionStore } from '$lib/stores/asset-interaction.store';
 
-	export let assets: AssetResponseDto[];
+	export let markers: MapMarkerResponseDto[];
 
 	const map = getMapContext();
 	let cluster: L.MarkerClusterGroup;
@@ -33,17 +33,11 @@
 			spiderfyDistanceMultiplier: 3
 		});
 
-		for (let asset of assets) {
-			if (!asset.exifInfo) continue;
-
-			const lat = asset.exifInfo.latitude;
-			const lon = asset.exifInfo.longitude;
-
-			if (!lat || !lon) continue;
+		for (let marker of markers) {
 
 			const icon = new Icon({
-				iconUrl: getFileUrl(asset.id, true),
-				iconRetinaUrl: getFileUrl(asset.id, true),
+				iconUrl: getFileUrl(marker.id, true),
+				iconRetinaUrl: getFileUrl(marker.id, true),
 				iconSize: [60, 60],
 				iconAnchor: [12, 41],
 				popupAnchor: [1, -34],
@@ -51,16 +45,16 @@
 				shadowSize: [41, 41]
 			});
 
-			const marker = new Marker([lat, lon], {
+			const leafletMarker = new Marker([marker.lat, marker.lon], {
 				icon,
 				alt: ''
 			});
 
-			marker.on('click', () => {
-				assetInteractionStore.setViewingAsset(asset);
+			leafletMarker.on('click', () => {
+				assetInteractionStore.setViewingAssetId(marker.id);
 			});
 
-			cluster.addLayer(marker);
+			cluster.addLayer(leafletMarker);
 		}
 
 		map.addLayer(cluster);

+ 3 - 2
web/src/lib/components/shared-components/leaflet/tile-layer.svelte

@@ -1,16 +1,17 @@
 <script lang="ts">
 	import { onDestroy, onMount } from 'svelte';
+	import 'leaflet.tilelayer.colorfilter';
 	import { TileLayer, type TileLayerOptions } from 'leaflet';
 	import { getMapContext } from './map.svelte';
 
 	export let urlTemplate: string;
-	export let options: TileLayerOptions | undefined = undefined;
+	export let options: any | undefined = undefined;
 	let tileLayer: TileLayer;
 
 	const map = getMapContext();
 
 	onMount(() => {
-		tileLayer = new TileLayer(urlTemplate, options).addTo(map);
+		tileLayer = new L.tileLayer.colorFilter(urlTemplate, options).addTo(map);
 	});
 
 	onDestroy(() => {

+ 7 - 2
web/src/lib/stores/asset-interaction.store.ts

@@ -53,10 +53,14 @@ function createAssetInteractionStore() {
 	 * Asset Viewer
 	 */
 	const setViewingAsset = async (asset: AssetResponseDto) => {
-		const { data } = await api.assetApi.getAssetById(asset.id);
+		setViewingAssetId(asset.id);
+	};
+
+	const setViewingAssetId = async (id: string) => {
+		const { data } = await api.assetApi.getAssetById(id);
 		viewingAssetStoreState.set(data);
 		isViewingAssetStoreState.set(true);
-	};
+	}
 
 	const setIsViewingAsset = (isViewing: boolean) => {
 		isViewingAssetStoreState.set(isViewing);
@@ -140,6 +144,7 @@ function createAssetInteractionStore() {
 
 	return {
 		setViewingAsset,
+		setViewingAssetId,
 		setIsViewingAsset,
 		navigateAsset,
 		addAssetToMultiselectGroup,

+ 2 - 2
web/src/routes/(user)/map/+page.server.ts

@@ -8,11 +8,11 @@ export const load = (async ({ locals: { api, user } }) => {
 	}
 
 	try {
-		const { data: assets } = await api.assetApi.getAllAssets();
+		const { data: mapMarkers } = await api.assetApi.getMapMarkers();
 
 		return {
 			user,
-			assets,
+			mapMarkers,
 			meta: {
 				title: 'Map'
 			}

+ 2 - 1
web/src/routes/(user)/map/+page.svelte

@@ -23,11 +23,12 @@
 			<TileLayer
 				urlTemplate={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
 				options={{
+					filter: ['bright:101%','contrast:101%','saturate:79%'],
 					attribution:
 						'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
 				}}
 			/>
-			<AssetMarkerCluster assets={data.assets} />
+			<AssetMarkerCluster markers={data.mapMarkers} />
 		</Map>
 	</div>