From 3bff1a438355eebf33858cdb3046a6ba383ea50c Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 24 Jun 2022 22:59:08 +0200 Subject: [PATCH] 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 {