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

This commit is contained in:
Matthias Rupp 2023-04-30 08:01:49 -11:00
parent 9a1e8e55f5
commit 1b1d2f0ba4
23 changed files with 727 additions and 23 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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';

View file

@ -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].

View file

@ -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':

View file

@ -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;
}

View file

@ -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

View file

@ -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
});
});
}

View file

@ -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.
*/

View file

@ -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,

View file

@ -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": {

View file

@ -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';

View file

@ -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
web/package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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);

View file

@ -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(() => {

View file

@ -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,

View file

@ -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'
}

View file

@ -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>