From 3bff1a438355eebf33858cdb3046a6ba383ea50c Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 24 Jun 2022 22:59:08 +0200 Subject: [PATCH 1/2] Implementing on-the-fly GPX compression. Its a lossy compression that can be finetuned in the config. #504 --- src/backend/middlewares/MetaFileMWs.ts | 40 ++++++ src/backend/model/GPXProcessing.ts | 127 ++++++++++++++++++ src/backend/routes/GalleryRouter.ts | 22 +++ src/common/config/private/PrivateConfig.ts | 127 ++++++++++-------- src/common/config/public/ClientConfig.ts | 11 ++ .../lightbox.map.gallery.component.ts | 34 ++--- .../app/ui/gallery/map/map.service.ts | 12 +- 7 files changed, 297 insertions(+), 76 deletions(-) create mode 100644 src/backend/middlewares/MetaFileMWs.ts create mode 100644 src/backend/model/GPXProcessing.ts diff --git a/src/backend/middlewares/MetaFileMWs.ts b/src/backend/middlewares/MetaFileMWs.ts new file mode 100644 index 00000000..5c352b8b --- /dev/null +++ b/src/backend/middlewares/MetaFileMWs.ts @@ -0,0 +1,40 @@ +import {NextFunction, Request, Response} from 'express'; +import * as fs from 'fs'; +import { Config } from '../../common/config/private/Config'; +import { GPXProcessing } from '../model/GPXProcessing'; + +export class MetaFileMWs { + public static async compressGPX( + req: Request, + res: Response, + next: NextFunction + ): Promise { + if (!req.resultPipe) { + return next(); + } + // if conversion is not enabled redirect, so browser can cache the full + if (Config.Client.MetaFile.GPXCompressing.enabled === false) { + return res.redirect(req.originalUrl.slice(0, -1 * '\\bestFit'.length)); + } + const fullPath = req.resultPipe as string; + + const compressedGPX = GPXProcessing.generateConvertedPath( + fullPath, + ); + + // check if converted photo exist + if (fs.existsSync(compressedGPX) === true) { + req.resultPipe = compressedGPX; + return next(); + } + + if (Config.Server.MetaFile.GPXCompressing.onTheFly === true) { + req.resultPipe = await GPXProcessing.compressGPX(fullPath); + return next(); + } + + // not converted and won't be now + return res.redirect(req.originalUrl.slice(0, -1 * '\\bestFit'.length)); + } +} + diff --git a/src/backend/model/GPXProcessing.ts b/src/backend/model/GPXProcessing.ts new file mode 100644 index 00000000..41c0e8ed --- /dev/null +++ b/src/backend/model/GPXProcessing.ts @@ -0,0 +1,127 @@ +import * as path from 'path'; +import {constants as fsConstants, promises as fsp} from 'fs'; +import * as xml2js from 'xml2js'; +import {ProjectPath} from '../ProjectPath'; +import {Config} from '../../common/config/private/Config'; + +type gpxEntry = { '$': { lat: string, lon: string }, ele: string[], time: string[], extensions: unknown }; + +export class GPXProcessing { + + + public static generateConvertedPath(filePath: string): string { + const file = path.basename(filePath); + return path.join( + ProjectPath.TranscodedFolder, + ProjectPath.getRelativePathToImages(path.dirname(filePath)), + file + ); + } + + public static async isValidConvertedPath( + convertedPath: string + ): Promise { + const origFilePath = path.join( + ProjectPath.ImageFolder, + path.relative( + ProjectPath.TranscodedFolder, + convertedPath + ) + ); + + + try { + await fsp.access(origFilePath, fsConstants.R_OK); + } catch (e) { + return false; + } + + return true; + } + + + static async compressedGPXExist( + filePath: string + ): Promise { + // compressed gpx path + const outPath = GPXProcessing.generateConvertedPath(filePath); + + // check if file already exist + try { + await fsp.access(outPath, fsConstants.R_OK); + return true; + } catch (e) { + // ignoring errors + } + return false; + } + + public static async compressGPX( + filePath: string, + ): Promise { + // generate compressed gpx path + const outPath = GPXProcessing.generateConvertedPath(filePath); + + // check if file already exist + try { + await fsp.access(outPath, fsConstants.R_OK); + return outPath; + } catch (e) { + // ignoring errors + } + + + const outDir = path.dirname(outPath); + + await fsp.mkdir(outDir, {recursive: true}); + const gpxStr = await fsp.readFile(filePath); + const gpxObj = await (new xml2js.Parser()).parseStringPromise(gpxStr); + const items: gpxEntry[] = gpxObj.gpx.trk[0].trkseg[0].trkpt; + + const distance = (entry1: gpxEntry, entry2: gpxEntry) => { + const lat1 = parseFloat(entry1.$.lat); + const lon1 = parseFloat(entry1.$.lon); + const lat2 = parseFloat(entry2.$.lat); + const lon2 = parseFloat(entry2.$.lon); + + // credits to: https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const φ1 = lat1 * Math.PI / 180; // φ, λ in radians + const φ2 = lat2 * Math.PI / 180; + const Δφ = (lat2 - lat1) * Math.PI / 180; + const Δλ = (lon2 - lon1) * Math.PI / 180; + + const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(Δλ / 2) * Math.sin(Δλ / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + const d = R * c; // in metres + return d; + }; + const gpxEntryFilter = (value: gpxEntry, i: number, list: gpxEntry[]) => { + if (i === 0 || i >= list.length - 1) { // always keep the first and last items + return true; + } + const timeDelta = (Date.parse(list[i].time[0]) - Date.parse(list[i - 1].time[0])); // mill sec. + const dist = distance(list[i - 1], list[i]); // meters + + return !(timeDelta < Config.Server.MetaFile.GPXCompressing.minTimeDistance && + dist < Config.Server.MetaFile.GPXCompressing.minDistance); + }; + + gpxObj.gpx.trk[0].trkseg[0].trkpt = items.filter(gpxEntryFilter).map((v) => { + v.$.lon = parseFloat(v.$.lon).toFixed(6); + v.$.lat = parseFloat(v.$.lat).toFixed(6); + delete v.ele; + delete v.extensions; + return v; + }); + + await fsp.writeFile(outPath, (new xml2js.Builder({renderOpts: {pretty: false}})).buildObject(gpxObj)); + + return outPath; + } + +} + diff --git a/src/backend/routes/GalleryRouter.ts b/src/backend/routes/GalleryRouter.ts index 664edf28..3d4da7d6 100644 --- a/src/backend/routes/GalleryRouter.ts +++ b/src/backend/routes/GalleryRouter.ts @@ -9,6 +9,7 @@ import { VersionMWs } from '../middlewares/VersionMWs'; import { SupportedFormats } from '../../common/SupportedFormats'; import { PhotoConverterMWs } from '../middlewares/thumbnail/PhotoConverterMWs'; import { ServerTimingMWs } from '../middlewares/ServerTimingMWs'; +import {MetaFileMWs} from '../middlewares/MetaFileMWs'; export class GalleryRouter { public static route(app: Express): void { @@ -21,6 +22,7 @@ export class GalleryRouter { this.addGetBestFitVideo(app); this.addGetVideo(app); this.addGetMetaFile(app); + this.addGetBestFitMetaFile(app); this.addRandom(app); this.addDirectoryList(app); this.addDirectoryZip(app); @@ -158,6 +160,26 @@ export class GalleryRouter { ); } + protected static addGetBestFitMetaFile(app: Express): void { + app.get( + [ + '/api/gallery/content/:mediaPath(*.(' + + SupportedFormats.MetaFiles.join('|') + + '))/bestFit', + ], + // common part + AuthenticationMWs.authenticate, + AuthenticationMWs.normalizePathParam('mediaPath'), + AuthenticationMWs.authorisePath('mediaPath', false), + + // specific part + GalleryMWs.loadFile, + MetaFileMWs.compressGPX, + ServerTimingMWs.addServerTiming, + RenderingMWs.renderFile + ); + } + protected static addRandom(app: Express): void { app.get( ['/api/gallery/random/:searchQueryDTO'], diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index 3825efd2..a58987b9 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -5,17 +5,17 @@ import { JobTrigger, JobTriggerType, } from '../../entities/job/JobScheduleDTO'; -import { ClientConfig } from '../public/ClientConfig'; -import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass'; -import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty'; -import { DefaultsJobs } from '../../entities/job/JobDTO'; +import {ClientConfig, ClientMetaFileConfig} from '../public/ClientConfig'; +import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass'; +import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; +import {DefaultsJobs} from '../../entities/job/JobDTO'; import { SearchQueryDTO, SearchQueryTypes, TextSearch, } from '../../entities/SearchQueryDTO'; -import { SortingMethods } from '../../entities/SortingMethods'; -import { UserRoles } from '../../entities/UserDTO'; +import {SortingMethods} from '../../entities/SortingMethods'; +import {UserRoles} from '../../entities/UserDTO'; export enum DatabaseType { memory = 1, @@ -71,15 +71,15 @@ export type videoFormatType = 'mp4' | 'webm'; @SubConfigClass() export class MySQLConfig { - @ConfigProperty({ envAlias: 'MYSQL_HOST' }) + @ConfigProperty({envAlias: 'MYSQL_HOST'}) host: string = 'localhost'; - @ConfigProperty({ envAlias: 'MYSQL_PORT', min: 0, max: 65535 }) + @ConfigProperty({envAlias: 'MYSQL_PORT', min: 0, max: 65535}) port: number = 3306; - @ConfigProperty({ envAlias: 'MYSQL_DATABASE' }) + @ConfigProperty({envAlias: 'MYSQL_DATABASE'}) database: string = 'pigallery2'; - @ConfigProperty({ envAlias: 'MYSQL_USERNAME' }) + @ConfigProperty({envAlias: 'MYSQL_USERNAME'}) username: string = ''; - @ConfigProperty({ envAlias: 'MYSQL_PASSWORD', type: 'password' }) + @ConfigProperty({envAlias: 'MYSQL_PASSWORD', type: 'password'}) password: string = ''; } @@ -94,13 +94,13 @@ export class UserConfig { @ConfigProperty() name: string; - @ConfigProperty({ type: UserRoles }) + @ConfigProperty({type: UserRoles}) role: UserRoles; - @ConfigProperty({ description: 'Unencrypted, temporary password' }) + @ConfigProperty({description: 'Unencrypted, temporary password'}) password: string; - @ConfigProperty({ description: 'Encrypted password' }) + @ConfigProperty({description: 'Encrypted password'}) encryptedPassword: string | undefined; constructor(name: string, password: string, role: UserRoles) { @@ -142,12 +142,31 @@ export class ServerDataBaseConfig { @SubConfigClass() export class ServerThumbnailConfig { - @ConfigProperty({ description: 'if true, photos will have better quality.' }) + @ConfigProperty({description: 'if true, photos will have better quality.'}) qualityPriority: boolean = true; - @ConfigProperty({ type: 'ratio' }) + @ConfigProperty({type: 'ratio'}) personFaceMargin: number = 0.6; // in ration [0-1] } +@SubConfigClass() +export class ServerGPXCompressingConfig { + @ConfigProperty({ + description: 'Compresses gpx files on-the-fly, when they are requested.', + }) + onTheFly: boolean = true; + @ConfigProperty({type: 'unsignedInt', description: 'Filters out entry that are closer than this in meters.'}) + minDistance: number = 5; + @ConfigProperty({type: 'unsignedInt', description: 'Filters out entry that are closer than this in time in milliseconds.'}) + minTimeDistance: number = 5000; +} + +@SubConfigClass() +export class ServerMetaFileConfig { + @ConfigProperty() + GPXCompressing: ServerGPXCompressingConfig = new ServerGPXCompressingConfig(); +} + + @SubConfigClass() export class ServerSharingConfig { @ConfigProperty() @@ -158,14 +177,14 @@ export class ServerSharingConfig { export class ServerIndexingConfig { @ConfigProperty() cachedFolderTimeout: number = 1000 * 60 * 60; // Do not rescans the folder if seems ok - @ConfigProperty({ type: ReIndexingSensitivity }) + @ConfigProperty({type: ReIndexingSensitivity}) reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low; @ConfigProperty({ arrayType: 'string', description: - "If an entry starts with '/' it is treated as an absolute path." + - " If it doesn't start with '/' but contains a '/', the path is relative to the image directory." + - " If it doesn't contain a '/', any folder with this name will be excluded.", + 'If an entry starts with \'/\' it is treated as an absolute path.' + + ' If it doesn\'t start with \'/\' but contains a \'/\', the path is relative to the image directory.' + + ' If it doesn\'t contain a \'/\', any folder with this name will be excluded.', }) excludeFolderList: string[] = ['.Trash-1000', '.dtrash', '$RECYCLE.BIN']; @ConfigProperty({ @@ -178,11 +197,11 @@ export class ServerIndexingConfig { @SubConfigClass() export class ServerThreadingConfig { - @ConfigProperty({ description: 'App can run on multiple thread' }) + @ConfigProperty({description: 'App can run on multiple thread'}) enabled: boolean = true; @ConfigProperty({ description: - "Number of threads that are used to generate thumbnails. If 0, number of 'CPU cores -1' threads will be used.", + 'Number of threads that are used to generate thumbnails. If 0, number of \'CPU cores -1\' threads will be used.', }) thumbnailThreads: number = 0; // if zero-> CPU count -1 } @@ -195,9 +214,9 @@ export class ServerDuplicatesConfig { @SubConfigClass() export class ServerLogConfig { - @ConfigProperty({ type: LogLevel }) + @ConfigProperty({type: LogLevel}) level: LogLevel = LogLevel.info; - @ConfigProperty({ type: SQLLogLevel }) + @ConfigProperty({type: SQLLogLevel}) sqlLevel: SQLLogLevel = SQLLogLevel.error; @ConfigProperty() logServerTiming: boolean = false; @@ -205,32 +224,32 @@ export class ServerLogConfig { @SubConfigClass() export class NeverJobTrigger implements JobTrigger { - @ConfigProperty({ type: JobTriggerType }) + @ConfigProperty({type: JobTriggerType}) readonly type = JobTriggerType.never; } @SubConfigClass() export class ScheduledJobTrigger implements JobTrigger { - @ConfigProperty({ type: JobTriggerType }) + @ConfigProperty({type: JobTriggerType}) readonly type = JobTriggerType.scheduled; - @ConfigProperty({ type: 'unsignedInt' }) + @ConfigProperty({type: 'unsignedInt'}) time: number; // data time } @SubConfigClass() export class PeriodicJobTrigger implements JobTrigger { - @ConfigProperty({ type: JobTriggerType }) + @ConfigProperty({type: JobTriggerType}) readonly type = JobTriggerType.periodic; - @ConfigProperty({ type: 'unsignedInt', max: 7 }) + @ConfigProperty({type: 'unsignedInt', max: 7}) periodicity: number | undefined; // 0-6: week days 7 every day - @ConfigProperty({ type: 'unsignedInt', max: 23 * 60 + 59 }) + @ConfigProperty({type: 'unsignedInt', max: 23 * 60 + 59}) atTime: number | undefined; // day time } @SubConfigClass() export class AfterJobTrigger implements JobTrigger { - @ConfigProperty({ type: JobTriggerType }) + @ConfigProperty({type: JobTriggerType}) readonly type = JobTriggerType.after; @ConfigProperty() afterScheduleName: string | undefined; // runs after schedule @@ -294,16 +313,16 @@ export class JobScheduleConfig implements JobScheduleDTO { @SubConfigClass() export class ServerJobConfig { - @ConfigProperty({ type: 'integer', description: 'Job history size' }) + @ConfigProperty({type: 'integer', description: 'Job history size'}) maxSavedProgress: number = 10; - @ConfigProperty({ arrayType: JobScheduleConfig }) + @ConfigProperty({arrayType: JobScheduleConfig}) scheduled: JobScheduleConfig[] = [ new JobScheduleConfig( DefaultsJobs[DefaultsJobs.Indexing], DefaultsJobs[DefaultsJobs.Indexing], false, new NeverJobTrigger(), - { indexChangesOnly: true } + {indexChangesOnly: true} ), new JobScheduleConfig( DefaultsJobs[DefaultsJobs['Preview Filling']], @@ -317,39 +336,39 @@ export class ServerJobConfig { DefaultsJobs[DefaultsJobs['Thumbnail Generation']], false, new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Preview Filling']]), - { sizes: [240], indexedOnly: true } + {sizes: [240], indexedOnly: true} ), new JobScheduleConfig( DefaultsJobs[DefaultsJobs['Photo Converting']], DefaultsJobs[DefaultsJobs['Photo Converting']], false, new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]), - { indexedOnly: true } + {indexedOnly: true} ), new JobScheduleConfig( DefaultsJobs[DefaultsJobs['Video Converting']], DefaultsJobs[DefaultsJobs['Video Converting']], false, new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Photo Converting']]), - { indexedOnly: true } + {indexedOnly: true} ), new JobScheduleConfig( DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], false, new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Video Converting']]), - { indexedOnly: true } + {indexedOnly: true} ), ]; } @SubConfigClass() export class VideoTranscodingConfig { - @ConfigProperty({ type: 'unsignedInt' }) + @ConfigProperty({type: 'unsignedInt'}) bitRate: number = 5 * 1024 * 1024; - @ConfigProperty({ type: 'unsignedInt' }) + @ConfigProperty({type: 'unsignedInt'}) resolution: videoResolutionType = 720; - @ConfigProperty({ type: 'positiveFloat' }) + @ConfigProperty({type: 'positiveFloat'}) fps: number = 25; @ConfigProperty() codec: videoCodecType = 'libx264'; @@ -388,7 +407,7 @@ export class PhotoConvertingConfig { description: 'Converts photos on the fly, when they are requested.', }) onTheFly: boolean = true; - @ConfigProperty({ type: 'unsignedInt' }) + @ConfigProperty({type: 'unsignedInt'}) resolution: videoResolutionType = 1080; } @@ -400,12 +419,12 @@ export class ServerPhotoConfig { @SubConfigClass() export class ServerPreviewConfig { - @ConfigProperty({ type: 'object' }) + @ConfigProperty({type: 'object'}) SearchQuery: SearchQueryDTO = { type: SearchQueryTypes.any_text, text: '', } as TextSearch; - @ConfigProperty({ arrayType: SortingMethods }) + @ConfigProperty({arrayType: SortingMethods}) Sorting: SortingMethods[] = [ SortingMethods.descRating, SortingMethods.descDate, @@ -434,25 +453,25 @@ export class ServerMediaConfig { @SubConfigClass() export class ServerEnvironmentConfig { - @ConfigProperty({ volatile: true }) + @ConfigProperty({volatile: true}) upTime: string | undefined; - @ConfigProperty({ volatile: true }) + @ConfigProperty({volatile: true}) appVersion: string | undefined; - @ConfigProperty({ volatile: true }) + @ConfigProperty({volatile: true}) buildTime: string | undefined; - @ConfigProperty({ volatile: true }) + @ConfigProperty({volatile: true}) buildCommitHash: string | undefined; - @ConfigProperty({ volatile: true }) + @ConfigProperty({volatile: true}) isDocker: boolean | undefined; } @SubConfigClass() export class ServerConfig { - @ConfigProperty({ volatile: true }) + @ConfigProperty({volatile: true}) Environment: ServerEnvironmentConfig = new ServerEnvironmentConfig(); - @ConfigProperty({ arrayType: 'string' }) + @ConfigProperty({arrayType: 'string'}) sessionSecret: string[] = []; - @ConfigProperty({ type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535 }) + @ConfigProperty({type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535}) port: number = 80; @ConfigProperty() host: string = '0.0.0.0'; @@ -466,7 +485,7 @@ export class ServerConfig { Database: ServerDataBaseConfig = new ServerDataBaseConfig(); @ConfigProperty() Sharing: ServerSharingConfig = new ServerSharingConfig(); - @ConfigProperty({ type: 'unsignedInt', description: 'unit: ms' }) + @ConfigProperty({type: 'unsignedInt', description: 'unit: ms'}) sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms @ConfigProperty() Indexing: ServerIndexingConfig = new ServerIndexingConfig(); @@ -482,6 +501,8 @@ export class ServerConfig { Log: ServerLogConfig = new ServerLogConfig(); @ConfigProperty() Jobs: ServerJobConfig = new ServerJobConfig(); + @ConfigProperty() + MetaFile: ServerMetaFileConfig = new ServerMetaFileConfig(); } export interface IPrivateConfig { diff --git a/src/common/config/public/ClientConfig.ts b/src/common/config/public/ClientConfig.ts index 34c9334e..5733cbce 100644 --- a/src/common/config/public/ClientConfig.ts +++ b/src/common/config/public/ClientConfig.ts @@ -182,6 +182,12 @@ export class ClientPhotoConfig { loadFullImageOnZoom: boolean = true; } +@SubConfigClass() +export class ClientGPXCompressingConfig { + @ConfigProperty() + enabled: boolean = true; +} + @SubConfigClass() export class ClientMediaConfig { @ConfigProperty() @@ -198,6 +204,11 @@ export class ClientMetaFileConfig { description: 'Reads *.gpx files and renders them on the map.', }) gpx: boolean = true; + @ConfigProperty({ + description: 'Reads *.gpx files and renders them on the map.', + }) + @ConfigProperty() + GPXCompressing: ClientGPXCompressingConfig = new ClientGPXCompressingConfig(); @ConfigProperty({ description: 'Reads *.md files in a directory and shows the next to the map.', diff --git a/src/frontend/app/ui/gallery/map/lightbox/lightbox.map.gallery.component.ts b/src/frontend/app/ui/gallery/map/lightbox/lightbox.map.gallery.component.ts index 2116f7af..ccad3acc 100644 --- a/src/frontend/app/ui/gallery/map/lightbox/lightbox.map.gallery.component.ts +++ b/src/frontend/app/ui/gallery/map/lightbox/lightbox.map.gallery.component.ts @@ -6,22 +6,22 @@ import { OnChanges, ViewChild, } from '@angular/core'; -import { PhotoDTO } from '../../../../../../common/entities/PhotoDTO'; -import { Dimension } from '../../../../model/IRenderable'; -import { FullScreenService } from '../../fullscreen.service'; +import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO'; +import {Dimension} from '../../../../model/IRenderable'; +import {FullScreenService} from '../../fullscreen.service'; import { IconThumbnail, Thumbnail, ThumbnailBase, ThumbnailManagerService, } from '../../thumbnailManager.service'; -import { MediaIcon } from '../../MediaIcon'; -import { Media } from '../../Media'; -import { PageHelper } from '../../../../model/page.helper'; -import { FileDTO } from '../../../../../../common/entities/FileDTO'; -import { Utils } from '../../../../../../common/Utils'; -import { Config } from '../../../../../../common/config/public/Config'; -import { MapService } from '../map.service'; +import {MediaIcon} from '../../MediaIcon'; +import {Media} from '../../Media'; +import {PageHelper} from '../../../../model/page.helper'; +import {FileDTO} from '../../../../../../common/entities/FileDTO'; +import {Utils} from '../../../../../../common/Utils'; +import {Config} from '../../../../../../common/config/public/Config'; +import {MapService} from '../map.service'; import { control, Control, @@ -41,7 +41,7 @@ import { polyline, tileLayer, } from 'leaflet'; -import { LeafletControlLayersConfig } from '@asymmetrik/ngx-leaflet'; +import {LeafletControlLayersConfig} from '@asymmetrik/ngx-leaflet'; @Component({ selector: 'app-gallery-map-lightbox', @@ -66,7 +66,7 @@ export class GalleryMapLightboxComponent implements OnChanges { public visible = false; public controllersVisible = false; public opacity = 1.0; - @ViewChild('root', { static: true }) elementRef: ElementRef; + @ViewChild('root', {static: true}) elementRef: ElementRef; public mapOptions: MapOptions = { zoom: 2, // setting max zoom is needed to MarkerCluster https://github.com/Leaflet/Leaflet.markercluster/issues/611 @@ -130,7 +130,7 @@ export class GalleryMapLightboxComponent implements OnChanges { ]; for (let i = 0; i < mapService.Layers.length; ++i) { const l = mapService.Layers[i]; - const tl = tileLayer(l.url, { attribution: mapService.Attributions }); + const tl = tileLayer(l.url, {attribution: mapService.Attributions}); if (i === 0) { this.mapOptions.layers.push(tl); } @@ -140,7 +140,7 @@ export class GalleryMapLightboxComponent implements OnChanges { this.mapLayerControl = control.layers( this.mapLayersControlOption.baseLayers, this.mapLayersControlOption.overlays, - { position: 'bottomright' } + {position: 'bottomright'} ); } @@ -215,7 +215,7 @@ export class GalleryMapLightboxComponent implements OnChanges { if ( PageHelper.ScrollY > to.top || PageHelper.ScrollY + GalleryMapLightboxComponent.getScreenHeight() < - to.top + to.top ) { PageHelper.ScrollY = to.top; } @@ -277,7 +277,7 @@ export class GalleryMapLightboxComponent implements OnChanges { `preview`; if (!mkr.getPopup()) { - mkr.bindPopup(photoPopup, { minWidth: width }); + mkr.bindPopup(photoPopup, {minWidth: width}); } else { mkr.setPopupContent(photoPopup); } @@ -293,7 +293,7 @@ export class GalleryMapLightboxComponent implements OnChanges { `; - mkr.bindPopup(noPhotoPopup, { minWidth: width }); + mkr.bindPopup(noPhotoPopup, {minWidth: width}); mkr.on('popupopen', () => { photoTh.load(); photoTh.CurrentlyWaiting = true; diff --git a/src/frontend/app/ui/gallery/map/map.service.ts b/src/frontend/app/ui/gallery/map/map.service.ts index f1aaa2a7..cd6edfe0 100644 --- a/src/frontend/app/ui/gallery/map/map.service.ts +++ b/src/frontend/app/ui/gallery/map/map.service.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; -import { NetworkService } from '../../../model/network/network.service'; -import { FileDTO } from '../../../../../common/entities/FileDTO'; -import { Utils } from '../../../../../common/Utils'; -import { Config } from '../../../../../common/config/public/Config'; +import {Injectable} from '@angular/core'; +import {NetworkService} from '../../../model/network/network.service'; +import {FileDTO} from '../../../../../common/entities/FileDTO'; +import {Utils} from '../../../../../common/Utils'; +import {Config} from '../../../../../common/config/public/Config'; import { MapLayers, MapProviders, } from '../../../../../common/config/public/ClientConfig'; -import { LatLngLiteral } from 'leaflet'; +import {LatLngLiteral} from 'leaflet'; @Injectable() export class MapService { From 19e23133f2530c379231b261bdb812ac9fd8c866 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 24 Jun 2022 23:15:48 +0200 Subject: [PATCH 2/2] Adding gpx compression buttons to the settings #504 --- src/backend/middlewares/admin/SettingsMWs.ts | 20 ++++-- .../model/diagnostics/ConfigDiagnostics.ts | 71 +++++++++++++------ src/common/config/public/ClientConfig.ts | 6 +- .../metafile.settings.component.html | 27 ++++++- .../metafiles/metafile.settings.component.ts | 13 +++- .../metafiles/metafile.settings.service.ts | 23 +++--- 6 files changed, 115 insertions(+), 45 deletions(-) diff --git a/src/backend/middlewares/admin/SettingsMWs.ts b/src/backend/middlewares/admin/SettingsMWs.ts index fcb98540..6b7f2067 100644 --- a/src/backend/middlewares/admin/SettingsMWs.ts +++ b/src/backend/middlewares/admin/SettingsMWs.ts @@ -12,7 +12,7 @@ import { DatabaseType, ServerDataBaseConfig, ServerIndexingConfig, - ServerJobConfig, + ServerJobConfig, ServerMetaFileConfig, ServerPhotoConfig, ServerPreviewConfig, ServerThumbnailConfig, @@ -21,7 +21,7 @@ import { import { ClientAlbumConfig, ClientFacesConfig, - ClientMapConfig, + ClientMapConfig, ClientMediaConfig, ClientMetaFileConfig, ClientPhotoConfig, ClientRandomPhotoConfig, @@ -166,13 +166,21 @@ export class SettingsMWs { } try { - const original = await Config.original(); - await ConfigDiagnostics.testMetaFileConfig(req.body.settings as ClientMetaFileConfig, original); + const settings: { + server: ServerMetaFileConfig, + client: ClientMetaFileConfig + } = req.body.settings; - Config.Client.MetaFile = (req.body.settings as ClientMetaFileConfig); + const original = await Config.original(); + await ConfigDiagnostics.testClientMetaFileConfig(settings.client, original); + await ConfigDiagnostics.testServerMetaFileConfig(settings.server, original); + + Config.Client.MetaFile = settings.client; + Config.Server.MetaFile = settings.server; // only updating explicitly set config (not saving config set by the diagnostics) - original.Client.MetaFile = (req.body.settings as ClientMetaFileConfig); + original.Client.MetaFile = settings.client; + original.Server.MetaFile = settings.server; original.save(); await ConfigDiagnostics.runDiagnostics(); Logger.info(LOG_TAG, 'new config:'); diff --git a/src/backend/model/diagnostics/ConfigDiagnostics.ts b/src/backend/model/diagnostics/ConfigDiagnostics.ts index 6bd6408b..7bba6cc9 100644 --- a/src/backend/model/diagnostics/ConfigDiagnostics.ts +++ b/src/backend/model/diagnostics/ConfigDiagnostics.ts @@ -1,9 +1,9 @@ -import { Config } from '../../../common/config/private/Config'; -import { Logger } from '../../Logger'; -import { NotificationManager } from '../NotifocationManager'; -import { SQLConnection } from '../database/sql/SQLConnection'; +import {Config} from '../../../common/config/private/Config'; +import {Logger} from '../../Logger'; +import {NotificationManager} from '../NotifocationManager'; +import {SQLConnection} from '../database/sql/SQLConnection'; import * as fs from 'fs'; -import { FFmpegFactory } from '../FFmpegFactory'; +import {FFmpegFactory} from '../FFmpegFactory'; import { ClientAlbumConfig, ClientFacesConfig, @@ -23,17 +23,18 @@ import { IPrivateConfig, ServerDataBaseConfig, ServerJobConfig, + ServerMetaFileConfig, ServerPhotoConfig, ServerPreviewConfig, ServerThumbnailConfig, ServerVideoConfig, } from '../../../common/config/private/PrivateConfig'; -import { SearchQueryParser } from '../../../common/SearchQueryParser'; +import {SearchQueryParser} from '../../../common/SearchQueryParser'; import { SearchQueryTypes, TextSearch, } from '../../../common/entities/SearchQueryDTO'; -import { Utils } from '../../../common/Utils'; +import {Utils} from '../../../common/Utils'; const LOG_TAG = '[ConfigDiagnostics]'; @@ -76,13 +77,13 @@ export class ConfigDiagnostics { } catch (e) { throw new Error( 'Cannot read or write sqlite storage file: ' + - SQLConnection.getSQLiteDB(databaseConfig) + SQLConnection.getSQLiteDB(databaseConfig) ); } } } - static async testMetaFileConfig( + static async testClientMetaFileConfig( metaFileConfig: ClientMetaFileConfig, config: IPrivateConfig ): Promise { @@ -91,6 +92,13 @@ export class ConfigDiagnostics { } } + static async testServerMetaFileConfig( + metaFileConfig: ServerMetaFileConfig, + config: IPrivateConfig + ): Promise { + // nothing to check at the moment + } + static testClientVideoConfig(videoConfig: ClientVideoConfig): Promise { return new Promise((resolve, reject) => { try { @@ -101,7 +109,7 @@ export class ConfigDiagnostics { return reject( new Error( 'Error accessing ffmpeg, cant find executable: ' + - err.toString() + err.toString() ) ); } @@ -110,7 +118,7 @@ export class ConfigDiagnostics { return reject( new Error( 'Error accessing ffmpeg-probe, cant find executable: ' + - err2.toString() + err2.toString() ) ); } @@ -150,7 +158,7 @@ export class ConfigDiagnostics { static testImageFolder(folder: string): Promise { return new Promise((resolve, reject) => { if (!fs.existsSync(folder)) { - reject("Images folder not exists: '" + folder + "'"); + reject('Images folder not exists: \'' + folder + '\''); } fs.access(folder, fs.constants.R_OK, (err) => { if (err) { @@ -327,8 +335,8 @@ export class ConfigDiagnostics { Logger.warn( LOG_TAG, 'Thumbnail hardware acceleration is not possible.' + - " 'sharp' node module is not found." + - ' Falling back temporally to JS based thumbnail generation' + ' \'sharp\' node module is not found.' + + ' Falling back temporally to JS based thumbnail generation' ); process.exit(1); } @@ -362,7 +370,7 @@ export class ConfigDiagnostics { } try { - await ConfigDiagnostics.testMetaFileConfig( + await ConfigDiagnostics.testClientMetaFileConfig( Config.Client.MetaFile, Config ); @@ -380,6 +388,25 @@ export class ConfigDiagnostics { Config.Client.MetaFile.gpx = false; } + try { + await ConfigDiagnostics.testServerMetaFileConfig( + Config.Server.MetaFile, + Config + ); + } catch (ex) { + const err: Error = ex; + NotificationManager.warning( + 'Meta file support error, switching off gpx..', + err.toString() + ); + Logger.warn( + LOG_TAG, + 'Meta file support error, switching off..', + err.toString() + ); + Config.Client.MetaFile.gpx = false; + } + try { await ConfigDiagnostics.testAlbumsConfig(Config.Client.Album, Config); } catch (ex) { @@ -419,7 +446,7 @@ export class ConfigDiagnostics { const err: Error = ex; NotificationManager.warning( 'Search is not supported with these settings. Disabling temporally. ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Logger.warn( @@ -455,7 +482,7 @@ export class ConfigDiagnostics { const err: Error = ex; NotificationManager.warning( 'Faces are not supported with these settings. Disabling temporally. ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Logger.warn( @@ -472,7 +499,7 @@ export class ConfigDiagnostics { const err: Error = ex; NotificationManager.warning( 'Some Tasks are not supported with these settings. Disabling temporally. ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Logger.warn( @@ -489,7 +516,7 @@ export class ConfigDiagnostics { const err: Error = ex; NotificationManager.warning( 'Sharing is not supported with these settings. Disabling temporally. ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Logger.warn( @@ -509,7 +536,7 @@ export class ConfigDiagnostics { const err: Error = ex; NotificationManager.warning( 'Random Media is not supported with these settings. Disabling temporally. ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Logger.warn( @@ -526,13 +553,13 @@ export class ConfigDiagnostics { const err: Error = ex; NotificationManager.warning( 'Maps is not supported with these settings. Using open street maps temporally. ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Logger.warn( LOG_TAG, 'Maps is not supported with these settings. Using open street maps temporally ' + - 'Please adjust the config properly.', + 'Please adjust the config properly.', err.toString() ); Config.Client.Map.mapProvider = MapProviders.OpenStreetMap; diff --git a/src/common/config/public/ClientConfig.ts b/src/common/config/public/ClientConfig.ts index 5733cbce..bbedb1c7 100644 --- a/src/common/config/public/ClientConfig.ts +++ b/src/common/config/public/ClientConfig.ts @@ -204,16 +204,16 @@ export class ClientMetaFileConfig { description: 'Reads *.gpx files and renders them on the map.', }) gpx: boolean = true; - @ConfigProperty({ - description: 'Reads *.gpx files and renders them on the map.', - }) + @ConfigProperty() GPXCompressing: ClientGPXCompressingConfig = new ClientGPXCompressingConfig(); + @ConfigProperty({ description: 'Reads *.md files in a directory and shows the next to the map.', }) markdown: boolean = true; + @ConfigProperty({ description: 'Reads *.pg2conf files (You can use it for custom sorting and save search (albums)).', diff --git a/src/frontend/app/ui/settings/metafiles/metafile.settings.component.html b/src/frontend/app/ui/settings/metafiles/metafile.settings.component.html index 5df8d362..423deb24 100644 --- a/src/frontend/app/ui/settings/metafiles/metafile.settings.component.html +++ b/src/frontend/app/ui/settings/metafiles/metafile.settings.component.html @@ -12,21 +12,42 @@ description="Reads *.gpx files and renders them on the map." i18n-description i18n-name [disabled]="!(settingsService.Settings | async).Client.Map.enabled" - [ngModel]="states.gpx"> + [ngModel]="states.client.gpx"> + + + + + + +
+ + [ngModel]="states.client.markdown"> + [ngModel]="states.client.pg2conf"> diff --git a/src/frontend/app/ui/settings/metafiles/metafile.settings.component.ts b/src/frontend/app/ui/settings/metafiles/metafile.settings.component.ts index 6c70496b..c931a22c 100644 --- a/src/frontend/app/ui/settings/metafiles/metafile.settings.component.ts +++ b/src/frontend/app/ui/settings/metafiles/metafile.settings.component.ts @@ -4,7 +4,8 @@ import { SettingsComponentDirective } from '../_abstract/abstract.settings.compo import { AuthenticationService } from '../../../model/network/authentication.service'; import { NavigationService } from '../../../model/navigation.service'; import { NotificationService } from '../../../model/notification.service'; -import { ClientMetaFileConfig } from '../../../../../common/config/public/ClientConfig'; +import {ClientMetaFileConfig, ClientPhotoConfig} from '../../../../../common/config/public/ClientConfig'; +import {ServerMetaFileConfig, ServerPhotoConfig} from '../../../../../common/config/private/PrivateConfig'; @Component({ selector: 'app-settings-meta-file', @@ -15,7 +16,10 @@ import { ClientMetaFileConfig } from '../../../../../common/config/public/Client ], providers: [MetaFileSettingsService], }) -export class MetaFileSettingsComponent extends SettingsComponentDirective { +export class MetaFileSettingsComponent extends SettingsComponentDirective<{ + server: ServerMetaFileConfig; + client: ClientMetaFileConfig; +}> { constructor( authService: AuthenticationService, navigation: NavigationService, @@ -29,7 +33,10 @@ export class MetaFileSettingsComponent extends SettingsComponentDirective s.Client.MetaFile + (s) => ({ + client: s.Client.MetaFile, + server: s.Server.MetaFile, + }) ); } } diff --git a/src/frontend/app/ui/settings/metafiles/metafile.settings.service.ts b/src/frontend/app/ui/settings/metafiles/metafile.settings.service.ts index 7ed471da..7f1b8220 100644 --- a/src/frontend/app/ui/settings/metafiles/metafile.settings.service.ts +++ b/src/frontend/app/ui/settings/metafiles/metafile.settings.service.ts @@ -1,11 +1,15 @@ -import { Injectable } from '@angular/core'; -import { NetworkService } from '../../../model/network/network.service'; -import { SettingsService } from '../settings.service'; -import { AbstractSettingsService } from '../_abstract/abstract.settings.service'; -import { ClientMetaFileConfig } from '../../../../../common/config/public/ClientConfig'; +import {Injectable} from '@angular/core'; +import {NetworkService} from '../../../model/network/network.service'; +import {SettingsService} from '../settings.service'; +import {AbstractSettingsService} from '../_abstract/abstract.settings.service'; +import {ClientMetaFileConfig} from '../../../../../common/config/public/ClientConfig'; +import {ServerMetaFileConfig} from '../../../../../common/config/private/PrivateConfig'; @Injectable() -export class MetaFileSettingsService extends AbstractSettingsService { +export class MetaFileSettingsService extends AbstractSettingsService<{ + server: ServerMetaFileConfig; + client: ClientMetaFileConfig; +}> { constructor( private networkService: NetworkService, settingsService: SettingsService @@ -21,7 +25,10 @@ export class MetaFileSettingsService extends AbstractSettingsService { - return this.networkService.putJson('/settings/metafile', { settings }); + public updateSettings(settings: { + server: ServerMetaFileConfig; + client: ClientMetaFileConfig; + }): Promise { + return this.networkService.putJson('/settings/metafile', {settings}); } }