Add reverse geocoding and show asset location on map in detail view (#43)

* Added reserve geocoding, location in search suggestion, and search by location
* Added mapbox sdk to app
* Added mapbox to image detailed view
This commit is contained in:
Alex
2022-03-10 16:09:03 -06:00
committed by GitHub
parent 251c92ff1e
commit 026f3c24e9
30 changed files with 12112 additions and 184 deletions

11715
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
},
"dependencies": {
"@mapbox/mapbox-sdk": "^0.13.3",
"@nestjs/bull": "^0.4.2",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.1.6",

View File

@@ -249,7 +249,7 @@ export class AssetService {
const possibleSearchTerm = new Set<String>();
const rows = await this.assetRepository.query(
`
select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type
select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
from assets a
left join exif e on a.id = e."assetId"
left join smart_info si on a.id = si."assetId"
@@ -274,6 +274,11 @@ export class AssetService {
// Make and model
possibleSearchTerm.add(row['make']?.toLowerCase());
possibleSearchTerm.add(row['model']?.toLowerCase());
// Location
possibleSearchTerm.add(row['city']?.toLowerCase());
possibleSearchTerm.add(row['state']?.toLowerCase());
possibleSearchTerm.add(row['country']?.toLowerCase());
});
return Array.from(possibleSearchTerm).filter((x) => x != null);

View File

@@ -61,6 +61,15 @@ export class ExifEntity {
@Column({ type: 'float', nullable: true })
longitude: number;
@Column({ nullable: true })
city: string;
@Column({ nullable: true })
state: string;
@Column({ nullable: true })
country: string;
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset: ExifEntity;

View File

@@ -1,9 +1,14 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { ServerInfoService } from './server-info.service';
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
@Controller('server-info')
export class ServerInfoController {
constructor(private readonly serverInfoService: ServerInfoService) {}
constructor(private readonly serverInfoService: ServerInfoService, private readonly configService: ConfigService) {}
@Get()
async getServerInfo() {
@@ -16,4 +21,13 @@ export class ServerInfoController {
res: 'pong',
};
}
@UseGuards(JwtAuthGuard)
@Get('/mapbox')
async getMapboxInfo() {
return {
isEnable: this.configService.get('ENABLE_MAPBOX'),
mapboxSecret: this.configService.get('MAPBOX_KEY'),
};
}
}

View File

@@ -6,29 +6,4 @@ import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}

View File

@@ -11,22 +11,4 @@ export class UserService {
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
create(createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
async findAll() {}
findOne(id: number) {
return `This action returns a #${id} user`;
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}

View File

@@ -11,5 +11,11 @@ export const immichAppConfig: ConfigModuleOptions = {
DB_DATABASE_NAME: Joi.string().required(),
UPLOAD_LOCATION: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
ENABLE_MAPBOX: Joi.boolean().required().valid(true, false),
MAPBOX_KEY: Joi.any().when('ENABLE_MAPBOX', {
is: true,
then: Joi.string().required(),
otherwise: Joi.string().optional,
}),
}),
};

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddRegionCityToExIf1646709533213 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
ADD COLUMN city varchar;
ALTER TABLE exif
ADD COLUMN state varchar;
ALTER TABLE exif
ADD COLUMN country varchar;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN city;
ALTER TABLE exif
DROP COLUMN state;
ALTER TABLE exif
DROP COLUMN country;
`);
}
}

View File

@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddLocationToExifTextSearch1646710459852 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN IF EXISTS exif_text_searchable_column;
ALTER TABLE exif
ADD COLUMN IF NOT EXISTS exif_text_searchable_column tsvector
GENERATED ALWAYS AS (
TO_TSVECTOR('english',
COALESCE(make, '') || ' ' ||
COALESCE(model, '') || ' ' ||
COALESCE(orientation, '') || ' ' ||
COALESCE("lensModel", '') || ' ' ||
COALESCE("city", '') || ' ' ||
COALESCE("state", '') || ' ' ||
COALESCE("country", '')
)
) STORED;
CREATE INDEX exif_text_searchable_idx
ON exif
USING GIN (exif_text_searchable_column);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE exif
DROP COLUMN IF EXISTS exif_text_searchable_column;
DROP INDEX IF EXISTS exif_text_searchable_idx ON exif;
`);
}
}

View File

@@ -6,14 +6,18 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { ConfigService } from '@nestjs/config';
import exifr from 'exifr';
import { readFile } from 'fs/promises';
import fs from 'fs';
import fs, { rmSync } from 'fs';
import { Logger } from '@nestjs/common';
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
import axios from 'axios';
import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity';
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
@Processor('background-task')
export class BackgroundTaskProcessor {
private geocodingClient: GeocodeService;
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@@ -25,7 +29,13 @@ export class BackgroundTaskProcessor {
private exifRepository: Repository<ExifEntity>,
private configService: ConfigService,
) {}
) {
if (this.configService.get('ENABLE_MAPBOX')) {
this.geocodingClient = mapboxGeocoding({
accessToken: this.configService.get('MAPBOX_KEY'),
});
}
}
@Process('extract-exif')
async extractExif(job: Job) {
@@ -55,6 +65,26 @@ export class BackgroundTaskProcessor {
newExif.latitude = exifData['latitude'] || null;
newExif.longitude = exifData['longitude'] || null;
// Reverse GeoCoding
if (this.configService.get('ENABLE_MAPBOX') && exifData['longitude'] && exifData['latitude']) {
const geoCodeInfo: MapiResponse = await this.geocodingClient
.reverseGeocode({
query: [exifData['longitude'], exifData['latitude']],
types: ['country', 'region', 'place'],
})
.send();
const res: [] = geoCodeInfo.body['features'];
const city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
const state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
const country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
newExif.city = city || null;
newExif.state = state || null;
newExif.country = country || null;
}
await this.exifRepository.save(newExif);
try {