feat(web): Global map showing all assets with geo information (#2355)

* First crude implementation of the global asset map in web

* Use single DOM element for all markers

* Minor layout changes

* Refactor

* Add asset viewer

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

* Remove sidebar icon flip

* Add dark theme support

* Center map to most recent asset

* Allow cluster viewing

* Fix linter errors

* Add newlines

* Fix ts errors

* Fix eslint error

* Run prettier

* Server code style

* Fix openapi mobile code generation issues

* Map markers test

* fix: Support video thumbnails

* Update API

* Review suggestions

* Review suggestions

* Linter error

* Chage mapMarker endpoint to map-marker

* Clean up leaflet imports
This commit is contained in:
Matthias Rupp
2023-05-06 03:33:30 +02:00
committed by GitHub
parent 15a498fd60
commit 65daf342df
28 changed files with 902 additions and 5 deletions

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('/map-marker')
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

@@ -1,7 +1,7 @@
import { IAssetRepository } from './asset-repository';
import { AssetService } from './asset.service';
import { QueryFailedError, Repository } from 'typeorm';
import { AssetEntity, AssetType } from '@app/infra/entities';
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
@@ -57,6 +57,9 @@ const _getAsset_1 = () => {
asset_1.webpPath = '';
asset_1.encodedVideoPath = '';
asset_1.duration = '0:00:00.000000';
asset_1.exifInfo = new ExifEntity();
asset_1.exifInfo.latitude = 49.533547;
asset_1.exifInfo.longitude = 10.703075;
return asset_1;
};
@@ -492,4 +495,17 @@ describe('AssetService', () => {
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
});
});
describe('get map markers', () => {
it('should get geo information of assets', async () => {
assetRepositoryMock.getAllByUserId.mockResolvedValue(_getAssets());
const markers = await sut.getMapMarkers(authStub.admin, {});
expect(markers).toHaveLength(1);
expect(markers[0].lat).toBe(_getAsset_1().exifInfo?.latitude);
expect(markers[0].lon).toBe(_getAsset_1().exifInfo?.longitude);
expect(markers[0].id).toBe(_getAsset_1().id);
});
});
});

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

@@ -2601,6 +2601,64 @@
]
}
},
"/asset/map-marker": {
"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",
@@ -5426,6 +5484,31 @@
"timeBucket"
]
},
"MapMarkerResponseDto": {
"type": "object",
"properties": {
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
},
"lat": {
"type": "number",
"format": "double"
},
"lon": {
"type": "number",
"format": "double"
},
"id": {
"type": "string"
}
},
"required": [
"type",
"lat",
"lon",
"id"
]
},
"UpdateAssetDto": {
"type": "object",
"properties": {

View File

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