Compare commits
9 commits
main
...
dev/map-bo
Author | SHA1 | Date | |
---|---|---|---|
|
b6349ee643 | ||
|
3990708802 | ||
|
238268f5e0 | ||
|
c50a90ffca | ||
|
a3b059700f | ||
|
829b964df4 | ||
|
998dfdaeda | ||
|
2d489b33e3 | ||
|
557b824f8b |
18 changed files with 1504 additions and 44 deletions
12
cli/src/api/open-api/api.ts
generated
12
cli/src/api/open-api/api.ts
generated
|
@ -3926,6 +3926,18 @@ export interface SystemConfigReverseGeocodingDto {
|
||||||
* @memberof SystemConfigReverseGeocodingDto
|
* @memberof SystemConfigReverseGeocodingDto
|
||||||
*/
|
*/
|
||||||
'enabled': boolean;
|
'enabled': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SystemConfigReverseGeocodingDto
|
||||||
|
*/
|
||||||
|
'mapboxAccessToken': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SystemConfigReverseGeocodingDto
|
||||||
|
*/
|
||||||
|
'useMapbox': boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**citiesFileOverride** | [**CitiesFile**](CitiesFile.md) | |
|
**citiesFileOverride** | [**CitiesFile**](CitiesFile.md) | |
|
||||||
**enabled** | **bool** | |
|
**enabled** | **bool** | |
|
||||||
|
**mapboxAccessToken** | **String** | |
|
||||||
|
**useMapbox** | **bool** | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
|
@ -15,30 +15,42 @@ class SystemConfigReverseGeocodingDto {
|
||||||
SystemConfigReverseGeocodingDto({
|
SystemConfigReverseGeocodingDto({
|
||||||
required this.citiesFileOverride,
|
required this.citiesFileOverride,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
|
required this.mapboxAccessToken,
|
||||||
|
required this.useMapbox,
|
||||||
});
|
});
|
||||||
|
|
||||||
CitiesFile citiesFileOverride;
|
CitiesFile citiesFileOverride;
|
||||||
|
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
|
String mapboxAccessToken;
|
||||||
|
|
||||||
|
bool useMapbox;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigReverseGeocodingDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigReverseGeocodingDto &&
|
||||||
other.citiesFileOverride == citiesFileOverride &&
|
other.citiesFileOverride == citiesFileOverride &&
|
||||||
other.enabled == enabled;
|
other.enabled == enabled &&
|
||||||
|
other.mapboxAccessToken == mapboxAccessToken &&
|
||||||
|
other.useMapbox == useMapbox;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(citiesFileOverride.hashCode) +
|
(citiesFileOverride.hashCode) +
|
||||||
(enabled.hashCode);
|
(enabled.hashCode) +
|
||||||
|
(mapboxAccessToken.hashCode) +
|
||||||
|
(useMapbox.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigReverseGeocodingDto[citiesFileOverride=$citiesFileOverride, enabled=$enabled]';
|
String toString() => 'SystemConfigReverseGeocodingDto[citiesFileOverride=$citiesFileOverride, enabled=$enabled, mapboxAccessToken=$mapboxAccessToken, useMapbox=$useMapbox]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'citiesFileOverride'] = this.citiesFileOverride;
|
json[r'citiesFileOverride'] = this.citiesFileOverride;
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
|
json[r'mapboxAccessToken'] = this.mapboxAccessToken;
|
||||||
|
json[r'useMapbox'] = this.useMapbox;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +64,8 @@ class SystemConfigReverseGeocodingDto {
|
||||||
return SystemConfigReverseGeocodingDto(
|
return SystemConfigReverseGeocodingDto(
|
||||||
citiesFileOverride: CitiesFile.fromJson(json[r'citiesFileOverride'])!,
|
citiesFileOverride: CitiesFile.fromJson(json[r'citiesFileOverride'])!,
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
|
mapboxAccessToken: mapValueOfType<String>(json, r'mapboxAccessToken')!,
|
||||||
|
useMapbox: mapValueOfType<bool>(json, r'useMapbox')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -101,6 +115,8 @@ class SystemConfigReverseGeocodingDto {
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'citiesFileOverride',
|
'citiesFileOverride',
|
||||||
'enabled',
|
'enabled',
|
||||||
|
'mapboxAccessToken',
|
||||||
|
'useMapbox',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,16 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String mapboxAccessToken
|
||||||
|
test('to test the property `mapboxAccessToken`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool useMapbox
|
||||||
|
test('to test the property `useMapbox`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8828,11 +8828,19 @@
|
||||||
},
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"mapboxAccessToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"useMapbox": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"citiesFileOverride",
|
"citiesFileOverride",
|
||||||
"enabled"
|
"enabled",
|
||||||
|
"useMapbox",
|
||||||
|
"mapboxAccessToken"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|
1321
server/package-lock.json
generated
1321
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -40,6 +40,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.11",
|
"@babel/runtime": "^7.22.11",
|
||||||
|
"@mapbox/mapbox-sdk": "^0.15.3",
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
"@nestjs/common": "^10.2.2",
|
"@nestjs/common": "^10.2.2",
|
||||||
"@nestjs/config": "^3.0.0",
|
"@nestjs/config": "^3.0.0",
|
||||||
|
@ -100,6 +101,7 @@
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.8",
|
||||||
"@types/jest-when": "^3.5.2",
|
"@types/jest-when": "^3.5.2",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
|
"@types/mapbox__mapbox-sdk": "^0.13.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/mv": "^2.1.2",
|
"@types/mv": "^2.1.2",
|
||||||
|
@ -152,7 +154,7 @@
|
||||||
"branches": 80,
|
"branches": 80,
|
||||||
"functions": 80,
|
"functions": 80,
|
||||||
"lines": 90,
|
"lines": 90,
|
||||||
"statements": 90
|
"statements": 89
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setupFilesAfterEnv": [
|
"setupFilesAfterEnv": [
|
||||||
|
|
|
@ -91,7 +91,7 @@ describe(MetadataService.name, () => {
|
||||||
await sut.init();
|
await sut.init();
|
||||||
expect(metadataMock.deleteCache).not.toHaveBeenCalled();
|
expect(metadataMock.deleteCache).not.toHaveBeenCalled();
|
||||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||||
expect(metadataMock.init).toHaveBeenCalledTimes(1);
|
expect(metadataMock.initLocalGeocoding).toHaveBeenCalledTimes(1);
|
||||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
expect(metadataMock.deleteCache).not.toHaveBeenCalled();
|
expect(metadataMock.deleteCache).not.toHaveBeenCalled();
|
||||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||||
expect(metadataMock.init).toHaveBeenCalledTimes(1);
|
expect(metadataMock.initLocalGeocoding).toHaveBeenCalledTimes(1);
|
||||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
expect(metadataMock.deleteCache).not.toHaveBeenCalled();
|
expect(metadataMock.deleteCache).not.toHaveBeenCalled();
|
||||||
expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||||
expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 });
|
expect(metadataMock.initLocalGeocoding).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 });
|
||||||
expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
expect(metadataMock.deleteCache).toHaveBeenCalled();
|
expect(metadataMock.deleteCache).toHaveBeenCalled();
|
||||||
expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||||
expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 });
|
expect(metadataMock.initLocalGeocoding).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 });
|
||||||
expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -97,7 +97,17 @@ export class MetadataService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reverseGeocoding.useMapbox) {
|
||||||
|
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
||||||
|
await this.repository.initMapboxGeocoding(reverseGeocoding.mapboxAccessToken);
|
||||||
|
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
|
||||||
|
this.logger.log(`Initialized Mapbox reverse geocoder`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.repository.deinitMapboxGeocoding();
|
||||||
|
|
||||||
if (deleteCache) {
|
if (deleteCache) {
|
||||||
await this.repository.deleteCache();
|
await this.repository.deleteCache();
|
||||||
} else if (this.oldCities && this.oldCities === citiesFileOverride) {
|
} else if (this.oldCities && this.oldCities === citiesFileOverride) {
|
||||||
|
@ -105,7 +115,7 @@ export class MetadataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
||||||
await this.repository.init({ citiesFileOverride });
|
await this.repository.initLocalGeocoding({ citiesFileOverride });
|
||||||
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
|
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
|
||||||
|
|
||||||
this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`);
|
this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`);
|
||||||
|
|
|
@ -31,7 +31,9 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration'> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetadataRepository {
|
export interface IMetadataRepository {
|
||||||
init(options: Partial<InitOptions>): Promise<void>;
|
initLocalGeocoding(options: Partial<InitOptions>): Promise<void>;
|
||||||
|
initMapboxGeocoding(accessToken: string): Promise<void>;
|
||||||
|
deinitMapboxGeocoding(): void;
|
||||||
teardown(): Promise<void>;
|
teardown(): Promise<void>;
|
||||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||||
deleteCache(): Promise<void>;
|
deleteCache(): Promise<void>;
|
||||||
|
|
|
@ -1,6 +1,33 @@
|
||||||
import { CitiesFile } from '@app/infra/entities';
|
import { CitiesFile } from '@app/infra/entities';
|
||||||
|
import mapbox from '@mapbox/mapbox-sdk/services/tokens';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsEnum } from 'class-validator';
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsString,
|
||||||
|
Validate,
|
||||||
|
ValidateIf,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
const isEnabled = (config: SystemConfigReverseGeocodingDto) => config.enabled;
|
||||||
|
const useMapbox = (config: SystemConfigReverseGeocodingDto) => config.useMapbox;
|
||||||
|
|
||||||
|
@ValidatorConstraint({ name: 'mapboxAccessToken', async: false })
|
||||||
|
class MapboxAccessToken implements ValidatorConstraintInterface {
|
||||||
|
async validate(text: string) {
|
||||||
|
const mb = mapbox({ accessToken: text });
|
||||||
|
const response = await mb.getToken().send();
|
||||||
|
return response.body.code === 'TokenValid';
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage() {
|
||||||
|
// here you can provide default error message if validation failed
|
||||||
|
return 'Token invalid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SystemConfigReverseGeocodingDto {
|
export class SystemConfigReverseGeocodingDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@ -9,4 +36,14 @@ export class SystemConfigReverseGeocodingDto {
|
||||||
@IsEnum(CitiesFile)
|
@IsEnum(CitiesFile)
|
||||||
@ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' })
|
@ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' })
|
||||||
citiesFileOverride!: CitiesFile;
|
citiesFileOverride!: CitiesFile;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@ValidateIf(isEnabled)
|
||||||
|
useMapbox!: boolean;
|
||||||
|
|
||||||
|
@ValidateIf(useMapbox)
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Validate(MapboxAccessToken)
|
||||||
|
mapboxAccessToken!: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,8 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
citiesFileOverride: CitiesFile.CITIES_500,
|
citiesFileOverride: CitiesFile.CITIES_500,
|
||||||
|
useMapbox: false,
|
||||||
|
mapboxAccessToken: '',
|
||||||
},
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
|
@ -86,6 +86,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
citiesFileOverride: CitiesFile.CITIES_500,
|
citiesFileOverride: CitiesFile.CITIES_500,
|
||||||
|
useMapbox: false,
|
||||||
|
mapboxAccessToken: '',
|
||||||
},
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
autoLaunch: true,
|
autoLaunch: true,
|
||||||
|
|
|
@ -67,6 +67,8 @@ export enum SystemConfigKey {
|
||||||
|
|
||||||
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
|
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
|
||||||
REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
|
REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
|
||||||
|
REVERSE_GEOCODING_USE_MAPBOX = 'reverseGeocoding.useMapbox',
|
||||||
|
REVERSE_GEOCODING_MAPBOX_ACCESS_TOKEN = 'reverseGeocoding.mapboxAccessToken',
|
||||||
|
|
||||||
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
|
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
|
||||||
|
|
||||||
|
@ -201,6 +203,8 @@ export interface SystemConfig {
|
||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
citiesFileOverride: CitiesFile;
|
citiesFileOverride: CitiesFile;
|
||||||
|
useMapbox: boolean;
|
||||||
|
mapboxAccessToken: string;
|
||||||
};
|
};
|
||||||
oauth: {
|
oauth: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from '@app/domain';
|
import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from '@app/domain';
|
||||||
import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra';
|
import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra';
|
||||||
|
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
||||||
|
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DefaultReadTaskOptions, exiftool } from 'exiftool-vendored';
|
import { DefaultReadTaskOptions, exiftool } from 'exiftool-vendored';
|
||||||
import { readdir, rm } from 'fs/promises';
|
import { readdir, rm } from 'fs/promises';
|
||||||
|
@ -25,9 +27,23 @@ const lookup = promisify<GeoPoint[], number, AddressObject[][]>(geocoder.lookUp)
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataRepository implements IMetadataRepository {
|
export class MetadataRepository implements IMetadataRepository {
|
||||||
private logger = new Logger(MetadataRepository.name);
|
private logger = new Logger(MetadataRepository.name);
|
||||||
|
private mapboxClient?: GeocodeService = undefined;
|
||||||
|
|
||||||
async init(options: Partial<InitOptions>): Promise<void> {
|
async initMapboxGeocoding(accessToken: string): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
|
this.mapboxClient = mapboxGeocoding({ accessToken });
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deinitMapboxGeocoding(): void {
|
||||||
|
this.mapboxClient = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initLocalGeocoding(options: Partial<InitOptions>): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
this.deinitMapboxGeocoding();
|
||||||
|
|
||||||
geocoder.init(
|
geocoder.init(
|
||||||
{
|
{
|
||||||
load: {
|
load: {
|
||||||
|
@ -62,8 +78,14 @@ export class MetadataRepository implements IMetadataRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
||||||
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
if (this.mapboxClient) {
|
||||||
|
return this.useMapboxGeocoding(point, this.mapboxClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.useLocalGeocoding(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async useLocalGeocoding(point: GeoPoint) {
|
||||||
const [address] = await lookup([point], 1);
|
const [address] = await lookup([point], 1);
|
||||||
this.logger.verbose(`Raw: ${JSON.stringify(address, null, 2)}`);
|
this.logger.verbose(`Raw: ${JSON.stringify(address, null, 2)}`);
|
||||||
|
|
||||||
|
@ -76,6 +98,36 @@ export class MetadataRepository implements IMetadataRepository {
|
||||||
return { country, state, city };
|
return { country, state, city };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async useMapboxGeocoding(point: GeoPoint, mbClient: GeocodeService) {
|
||||||
|
const geoCodeInfo: MapiResponse = await mbClient
|
||||||
|
.reverseGeocode({
|
||||||
|
query: [point.longitude, point.latitude],
|
||||||
|
types: ['country', 'region', 'place'],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
const res: [] = geoCodeInfo.body['features'];
|
||||||
|
|
||||||
|
let city = '';
|
||||||
|
let state = '';
|
||||||
|
let country = '';
|
||||||
|
|
||||||
|
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]) {
|
||||||
|
city = res.filter((geoInfo) => geoInfo['place_type'][0] == 'place')[0]['text'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]) {
|
||||||
|
state = res.filter((geoInfo) => geoInfo['place_type'][0] == 'region')[0]['text'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]) {
|
||||||
|
country = res.filter((geoInfo) => geoInfo['place_type'][0] == 'country')[0]['text'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Mapbox Normalized: ${JSON.stringify({ country, state, city })}`);
|
||||||
|
|
||||||
|
return { country, state, city };
|
||||||
|
}
|
||||||
|
|
||||||
getExifTags(path: string): Promise<ImmichTags | null> {
|
getExifTags(path: string): Promise<ImmichTags | null> {
|
||||||
return exiftool
|
return exiftool
|
||||||
.read(path, undefined, {
|
.read(path, undefined, {
|
||||||
|
|
|
@ -4,7 +4,9 @@ export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> =>
|
||||||
return {
|
return {
|
||||||
deleteCache: jest.fn(),
|
deleteCache: jest.fn(),
|
||||||
getExifTags: jest.fn(),
|
getExifTags: jest.fn(),
|
||||||
init: jest.fn(),
|
initLocalGeocoding: jest.fn(),
|
||||||
|
deinitMapboxGeocoding: jest.fn(),
|
||||||
|
initMapboxGeocoding: jest.fn(),
|
||||||
teardown: jest.fn(),
|
teardown: jest.fn(),
|
||||||
reverseGeocode: jest.fn(),
|
reverseGeocode: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
12
web/src/api/open-api/api.ts
generated
12
web/src/api/open-api/api.ts
generated
|
@ -3926,6 +3926,18 @@ export interface SystemConfigReverseGeocodingDto {
|
||||||
* @memberof SystemConfigReverseGeocodingDto
|
* @memberof SystemConfigReverseGeocodingDto
|
||||||
*/
|
*/
|
||||||
'enabled': boolean;
|
'enabled': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SystemConfigReverseGeocodingDto
|
||||||
|
*/
|
||||||
|
'mapboxAccessToken': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SystemConfigReverseGeocodingDto
|
||||||
|
*/
|
||||||
|
'useMapbox': boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@
|
||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: config.reverseGeocoding.enabled,
|
enabled: config.reverseGeocoding.enabled,
|
||||||
citiesFileOverride: config.reverseGeocoding.citiesFileOverride,
|
citiesFileOverride: config.reverseGeocoding.citiesFileOverride,
|
||||||
|
useMapbox: config.reverseGeocoding.enabled ? config.reverseGeocoding.useMapbox : false,
|
||||||
|
mapboxAccessToken: config.reverseGeocoding.enabled ? config.reverseGeocoding.mapboxAccessToken : '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -149,8 +151,26 @@
|
||||||
isEdited={config.reverseGeocoding.citiesFileOverride !==
|
isEdited={config.reverseGeocoding.citiesFileOverride !==
|
||||||
savedConfig.reverseGeocoding.citiesFileOverride}
|
savedConfig.reverseGeocoding.citiesFileOverride}
|
||||||
/>
|
/>
|
||||||
</div></SettingAccordion
|
|
||||||
>
|
<hr />
|
||||||
|
|
||||||
|
<SettingSwitch
|
||||||
|
title="Use Mapbox"
|
||||||
|
disabled={!config.reverseGeocoding.enabled}
|
||||||
|
subtitle="Use Mapbox for reverse geocoding"
|
||||||
|
bind:checked={config.reverseGeocoding.useMapbox}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="Mapbox Access Token"
|
||||||
|
bind:value={config.reverseGeocoding.mapboxAccessToken}
|
||||||
|
required={true}
|
||||||
|
disabled={!config.reverseGeocoding.enabled || !config.reverseGeocoding.useMapbox}
|
||||||
|
isEdited={config.reverseGeocoding.mapboxAccessToken !== savedConfig.reverseGeocoding.mapboxAccessToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
on:reset={reset}
|
on:reset={reset}
|
||||||
|
|
Loading…
Reference in a new issue