mirror of
https://github.com/immich-app/immich.git
synced 2026-03-04 09:57:33 +03:00
feat(server): country geocoding for remote locations (#10950)
Co-authored-by: Zack Pollard <zackpollard@ymail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
@@ -7,6 +7,7 @@ import readLine from 'node:readline';
|
||||
import { citiesFile, resourcePaths } from 'src/constants';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import {
|
||||
@@ -28,6 +29,8 @@ export class MapRepository implements IMapRepository {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
|
||||
@InjectRepository(NaturalEarthCountriesEntity)
|
||||
private naturalEarthCountriesRepository: Repository<NaturalEarthCountriesEntity>,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@@ -46,6 +49,7 @@ export class MapRepository implements IMapRepository {
|
||||
}
|
||||
|
||||
await this.importGeodata();
|
||||
await this.importNaturalEarthCountries();
|
||||
|
||||
await this.metadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, {
|
||||
lastUpdate: geodataDate,
|
||||
@@ -130,22 +134,93 @@ export class MapRepository implements IMapRepository {
|
||||
.limit(1)
|
||||
.getOne();
|
||||
|
||||
if (!response) {
|
||||
if (response) {
|
||||
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
|
||||
|
||||
const { countryCode, name: city, admin1Name } = response;
|
||||
const country = getName(countryCode, 'en') ?? null;
|
||||
const state = admin1Name;
|
||||
|
||||
return { country, state, city };
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
||||
);
|
||||
|
||||
const ne_response = await this.naturalEarthCountriesRepository
|
||||
.createQueryBuilder('naturalearth_countries')
|
||||
.where('coordinates @> point (:longitude, :latitude)', point)
|
||||
.limit(1)
|
||||
.getOne();
|
||||
|
||||
if (!ne_response) {
|
||||
this.logger.warn(
|
||||
`Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
||||
`Response from database for natural earth reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
|
||||
this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
|
||||
|
||||
const { countryCode, name: city, admin1Name } = response;
|
||||
const country = getName(countryCode, 'en') ?? null;
|
||||
const state = admin1Name;
|
||||
const { admin_a3 } = ne_response;
|
||||
const country = getName(admin_a3, 'en') ?? null;
|
||||
const state = null;
|
||||
const city = null;
|
||||
|
||||
return { country, state, city };
|
||||
}
|
||||
|
||||
private transformCoordinatesToPolygon(coordinates: number[][][]): string {
|
||||
const pointsString = coordinates.map((point) => `(${point[0]},${point[1]})`).join(', ');
|
||||
return `(${pointsString})`;
|
||||
}
|
||||
|
||||
private async importNaturalEarthCountries() {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
await queryRunner.manager.clear(NaturalEarthCountriesEntity);
|
||||
|
||||
const fileContent = await readFile(resourcePaths.geodata.naturalEarthCountriesPath, 'utf8');
|
||||
const geoJSONData = JSON.parse(fileContent);
|
||||
|
||||
if (geoJSONData.type !== 'FeatureCollection' || !Array.isArray(geoJSONData.features)) {
|
||||
this.logger.fatal('Invalid GeoJSON FeatureCollection');
|
||||
return;
|
||||
}
|
||||
|
||||
for await (const feature of geoJSONData.features) {
|
||||
for (const polygon of feature.geometry.coordinates) {
|
||||
const featureRecord = new NaturalEarthCountriesEntity();
|
||||
featureRecord.admin = feature.properties.ADMIN;
|
||||
featureRecord.admin_a3 = feature.properties.ADM0_A3;
|
||||
featureRecord.type = feature.properties.TYPE;
|
||||
|
||||
if (feature.geometry.type === 'MultiPolygon') {
|
||||
featureRecord.coordinates = this.transformCoordinatesToPolygon(polygon[0]);
|
||||
await queryRunner.manager.save(featureRecord);
|
||||
} else if (feature.geometry.type === 'Polygon') {
|
||||
featureRecord.coordinates = this.transformCoordinatesToPolygon(polygon);
|
||||
await queryRunner.manager.save(featureRecord);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
this.logger.fatal('Error importing natural earth country data', error);
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async importGeodata() {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
|
||||
Reference in New Issue
Block a user