feat(web+server): map improvements (#2498)

* feat(web+server): map improvements

* add number format double to fix mobile
This commit is contained in:
Michel Heusschen 2023-05-21 08:26:06 +02:00 committed by GitHub
parent e028cf9002
commit a7b9adc692
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 501 additions and 364 deletions

View file

@ -12,6 +12,16 @@ export interface LivePhotoSearchOptions {
type: AssetType;
}
export interface MapMarkerSearchOptions {
isFavorite?: boolean;
}
export interface MapMarker {
id: string;
lat: number;
lon: number;
}
export enum WithoutProperty {
THUMBNAIL = 'thumbnail',
ENCODED_VIDEO = 'encoded-video',
@ -31,4 +41,5 @@ export interface IAssetRepository {
getAll(options?: AssetSearchOptions): Promise<AssetEntity[]>;
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
}

View file

@ -1,5 +1,5 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { assetEntityStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
import { assetEntityStub, authStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
import { AssetService, IAssetRepository } from '../asset';
import { IJobRepository, JobName } from '../job';
@ -58,4 +58,29 @@ describe(AssetService.name, () => {
});
});
});
describe('get map markers', () => {
it('should get geo information of assets', async () => {
assetMock.getMapMarkers.mockResolvedValue(
[assetEntityStub.withLocation].map((asset) => ({
id: asset.id,
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
lat: asset.exifInfo!.latitude!,
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
lon: asset.exifInfo!.longitude!,
})),
);
const markers = await sut.getMapMarkers(authStub.user1, {});
expect(markers).toHaveLength(1);
expect(markers[0]).toEqual({
id: assetEntityStub.withLocation.id,
lat: 100,
lon: 100,
});
});
});
});

View file

@ -1,14 +1,17 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { Inject } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { IAssetUploadedJob, IJobRepository, JobName } from '../job';
import { AssetCore } from './asset.core';
import { IAssetRepository } from './asset.repository';
import { MapMarkerDto } from './dto/map-marker.dto';
import { MapMarkerResponseDto } from './response-dto';
export class AssetService {
private assetCore: AssetCore;
constructor(
@Inject(IAssetRepository) assetRepository: IAssetRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {
this.assetCore = new AssetCore(assetRepository, jobRepository);
@ -28,4 +31,8 @@ export class AssetService {
save(asset: Partial<AssetEntity>) {
return this.assetCore.save(asset);
}
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.assetRepository.getMapMarkers(authUser.id, options);
}
}

View file

@ -0,0 +1,10 @@
import { toBoolean } from 'apps/immich/src/utils/transform.util';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
export class MapMarkerDto {
@IsOptional()
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean;
}

View file

@ -1,35 +1,12 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
export class MapMarkerResponseDto {
@ApiProperty()
id!: string;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ format: 'double' })
lat!: number;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ format: 'double' })
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,
};
}

View file

@ -9,5 +9,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
deleteAll: jest.fn(),
save: jest.fn(),
findLivePhotoMatch: jest.fn(),
getMapMarkers: jest.fn(),
};
};

View file

@ -12,6 +12,7 @@ import {
UserEntity,
UserTokenEntity,
AssetFaceEntity,
ExifEntity,
} from '@app/infra/entities';
import {
AlbumResponseDto,
@ -220,6 +221,38 @@ export const assetEntityStub = {
fileModifiedAt: '2022-06-19T23:41:36.910Z',
fileCreatedAt: '2022-06-19T23:41:36.910Z',
} as AssetEntity),
withLocation: Object.freeze<AssetEntity>({
id: 'asset-with-favorite-id',
deviceAssetId: 'device-asset-id',
fileModifiedAt: '2023-02-23T05:06:29.716Z',
fileCreatedAt: '2023-02-23T05:06:29.716Z',
owner: userEntityStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext',
type: AssetType.IMAGE,
webpPath: null,
encodedVideoPath: null,
createdAt: '2023-02-23T05:06:29.716Z',
updatedAt: '2023-02-23T05:06:29.716Z',
mimeType: null,
isFavorite: false,
isArchived: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.ext',
faces: [],
exifInfo: {
latitude: 100,
longitude: 100,
} as ExifEntity,
}),
};
export const albumStub = {

View file

@ -1,4 +1,11 @@
import { AssetSearchOptions, IAssetRepository, LivePhotoSearchOptions, WithoutProperty } from '@app/domain';
import {
AssetSearchOptions,
IAssetRepository,
LivePhotoSearchOptions,
MapMarker,
MapMarkerSearchOptions,
WithoutProperty,
} from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
@ -21,7 +28,6 @@ export class AssetRepository implements IAssetRepository {
},
});
}
async deleteAll(ownerId: string): Promise<void> {
await this.repository.delete({ ownerId });
}
@ -166,4 +172,44 @@ export class AssetRepository implements IAssetRepository {
order: { fileCreatedAt: 'DESC' },
});
}
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
const { isFavorite } = options;
const assets = await this.repository.find({
select: {
id: true,
exifInfo: {
latitude: true,
longitude: true,
},
},
where: {
ownerId,
isVisible: true,
isArchived: false,
exifInfo: {
latitude: Not(IsNull()),
longitude: Not(IsNull()),
},
isFavorite,
},
relations: {
exifInfo: true,
},
order: {
fileCreatedAt: 'DESC',
},
});
return assets.map((asset) => ({
id: asset.id,
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
lat: asset.exifInfo!.latitude!,
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
lon: asset.exifInfo!.longitude!,
}));
}
}